fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

Not rendering multi-select in API doc while using Pydantic model

Open jerinpetergeorge opened this issue 2 years ago • 16 comments

First Check

  • [X] I added a very descriptive title to this issue.
  • [X] I used the GitHub search to find a similar issue and didn't find it.
  • [X] I searched the FastAPI documentation, with the integrated search.
  • [X] I already searched in Google "How to X in FastAPI" and didn't find any information.
  • [X] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [X] I already checked if it is not related to FastAPI but to Pydantic.
  • [X] I already checked if it is not related to FastAPI but to Swagger UI.
  • [X] I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • [X] I commit to help with one of those options 👆

Example Code

import typing
from fastapi import FastAPI, Query, Depends
from pydantic import BaseModel
from enum import Enum

app = FastAPI()


class Status(str, Enum):
    SUCCESS = "SUCCESS"
    REFUND = "REFUND"
    FAIL = "FAIL"
    CANCEL = "CANCEL"


@app.get("/working-example/")
async def root_with_normal_query_params(status_in: typing.List[Status] = Query(...)):
    return {"status_inputs": status_in}


class StatusModel(BaseModel):
    status_in: typing.List[Status]


@app.get("/not-working-example/")
async def root_with_pydantic(status_inputs: StatusModel = Depends()):
    return {"status_inputs": status_inputs}

Description

The API docs are not generating the multi-select option while using the Pydantic model for the query/request parser.

without using Pydantic model

image

with using Pydantic model

image

Operating System

Linux

Operating System Details

No response

FastAPI Version

0.70.1

Python Version

Python 3.9.11

Additional Context

No response

jerinpetergeorge avatar Jun 16 '22 11:06 jerinpetergeorge

Hmm that is interesting. I reproduced it but I didn't had the time to follow the logic of FastAPI when it resolves the dependencies and the connection of that logic to the buildup of the openapi.json. I'll have another try tomorrow if I have the time, it shouldn't behave like that.

JarroVGIT avatar Jun 20 '22 20:06 JarroVGIT

Any thoughts @JarroVGIT :)

jerinpetergeorge avatar Jun 22 '22 18:06 jerinpetergeorge

@JarroVGIT @jerinpetergeorge Guys, I think you missed the point

Working example you provided is QUERY parameters

And non working example you provided is REQUEST BODY (not QUERY) and in docs it provides Example value and Schema If you click on Schema you will see all your choices from Status enum

image

Additional note: HTTP GET can not go with request body

zoliknemet avatar Jun 22 '22 19:06 zoliknemet

I figured that as well, but unfortunately that does not seem to be the case. The docs are saying us that when defining a param with a default value of Query(), it will be treated as a query list. The docs are also saying you can set this on a Pydantic model, and that is where the bug is. See the following example as illustration:

from re import S
import typing
from fastapi import FastAPI, Query, Depends, File
from pydantic import BaseModel, Field
from enum import Enum

app = FastAPI()

#----------------------------------------------
class Status(Enum):
    SUCCESS = "SUCCESS"
    REFUND = "REFUND"
    FAIL = "FAIL"
    CANCEL = "CANCEL"

#class with list of Enum:
class StatusModelWithListEnum(BaseModel):
    status_in: list[Status] = Query()

#class with Enum:
class StatusModelNoList(BaseModel):
    status_in: Status = Query(...)

#class with list of str:
class OtherModelWithListStr(BaseModel):
    some_param: list[str] = Query(...)

#class with str:
class OtherModelNoList(BaseModel):
    some_param: list[str] = Query(...)


#----------------------------------------------
#The following 4 endpoints do not show correctly in the docs, 
#as they are referencing pydantic models.
@app.get("/statusmodel-list")
async def statusmodel_list(par: StatusModelWithListEnum = Depends()):
    return {"status_inputs": par}
    #shows as request body.

@app.get("/statusmodel-nolist")
async def statusmodel_nolist(par: StatusModelNoList = Depends()):
    return {"status_inputs": par}
    #shows as query

@app.get("/stringmodel-list")
async def stringmodel_list(par: OtherModelWithListStr = Depends()):
    return {"status_inputs": par}
    #shows as request body

@app.get("/stringmodel-nolist")
async def stringmodel_nolist(par: StatusModelNoList = Depends()):
    return {"status_inputs": par}
    #shows as query

@app.get("/no-model-string-list")
async def no_model_string_list(par: list[str] = Query()):
    return {"status_inputs": par}
    #as per docs, shows as query!


#----------------------------------------------
#The following two endpoint will show correctly.
@app.get("/status-list-direct-in-param")
async def status_list_in_param(par: list[Status] = Query()):
    return {"status_inputs": par}

@app.get("/status-nolist")
async def stringmodel_nolist(par: Status = Query()):
    return {"status_inputs": par}


#----------------------------------------------
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000, )    

The issue is real, it shouldn't behave this way.

JarroVGIT avatar Jun 22 '22 20:06 JarroVGIT

@JarroVGIT yeah, I get it now!

zoliknemet avatar Jun 23 '22 09:06 zoliknemet

I think using Pydantic shows everything in body. I may be wrong.

iudeen avatar Jul 07 '22 23:07 iudeen

Is there a fix in the works for this issue?

bshea5 avatar Jul 13 '22 18:07 bshea5

Not AFAIK

JarroVGIT avatar Jul 13 '22 19:07 JarroVGIT

@jerinpetergeorge Seems like using a Dataclass instead can get around this issue if you're able to make the swap.

bshea5 avatar Jul 19 '22 21:07 bshea5

@jerinpetergeorge Seems like using a Dataclass instead can get around this issue if you're able to make the swap.

How? Can you give an example?

I tried these - it didn't seem to work

  •   from dataclasses import dataclass
    
      @dataclass
      class StatusModel:
          status_in: typing.List[Status]
    
  •  from dataclasses import dataclass
    
     @dataclass
     class StatusModel(BaseModel):
         status_in: typing.List[Status]
    

Moreover, the reason why I choose the Pydantic is that the superpower to parse the inputs - without the Pydantic model/feature I would be much disappointed :disappointed: @bshea5

jerinpetergeorge avatar Jul 20 '22 04:07 jerinpetergeorge

You can use dataclass from Pydantic.

import typing
from enum import Enum

from fastapi import FastAPI, Query, Depends
from pydantic.dataclasses import dataclass

app = FastAPI()


class Status(str, Enum):
    SUCCESS = "SUCCESS"
    REFUND = "REFUND"
    FAIL = "FAIL"
    CANCEL = "CANCEL"


@app.get("/working-example/")
async def root_with_normal_query_params(status_in: typing.List[Status] = Query(...)):
    return {"status_inputs": status_in}


@dataclass
class StatusModel:
    status_in: list[Status] = Query(...)


@app.get("/not-working-example/")  # it now works
async def root_with_pydantic(status_inputs: StatusModel = Depends()):
    return {"status_inputs": status_inputs}

iudeen avatar Jul 20 '22 04:07 iudeen

You can use dataclass from Pydantic.

import typing
from enum import Enum

from fastapi import FastAPI, Query, Depends
from pydantic.dataclasses import dataclass

app = FastAPI()


class Status(str, Enum):
    SUCCESS = "SUCCESS"
    REFUND = "REFUND"
    FAIL = "FAIL"
    CANCEL = "CANCEL"


@app.get("/working-example/")
async def root_with_normal_query_params(status_in: typing.List[Status] = Query(...)):
    return {"status_inputs": status_in}


@dataclass
class StatusModel:
    status_in: list[Status] = Query(...)


@app.get("/not-working-example/")  # it now works
async def root_with_pydantic(status_inputs: StatusModel = Depends()):
    return {"status_inputs": status_inputs}

This is nice!!!

Anyway, I'd love to see FastAPI supports the BaseModel as well (as per the issue described above)

jerinpetergeorge avatar Jul 20 '22 05:07 jerinpetergeorge

@iudeen : Nice solution! Do you know, by any chance, why this is working while the 'normal' approach is not? What makes pydantic.dataclasses.dataclass so different than pydantic.BaseModel? I've seen numerous issues where the issue is founded in the difference between the two (and sometimes also between Python's own dataclasses.dataclass and I am pretty confused on what the differences are and how they are handled differently by FastAPI. This is a prime example of that.

JarroVGIT avatar Jul 20 '22 05:07 JarroVGIT

@JarroVGIT this discussion is about the difference between the two..

iudeen avatar Jul 20 '22 05:07 iudeen

@jerinpetergeorge if you are happy with the solution, you can close this and open a ISSUE/BUG to discuss on why the "normal" approach is not working, and probably finding a better solution!

iudeen avatar Jul 20 '22 05:07 iudeen

@jerinpetergeorge if you are happy with the solution, you can close this and open a ISSUE/BUG to discuss on why the "normal" approach is not working, and probably finding a better solution!

I'm kind of okay with the solution since we're still not sure "why" FastAPI doesn't support it - is that on purpose? Or a bug?

Also, I hope this current GH issue considers a bug - or I wrote like that in the first place, which makes me think that it is better not to close this GH issue at the moment.

I would be super happy if someone could change the label of this issue from https://github.com/tiangolo/fastapi/labels/question to https://github.com/tiangolo/fastapi/labels/bug

jerinpetergeorge avatar Jul 20 '22 06:07 jerinpetergeorge