Using pydantic models for GET request query params? Currently not possible, have to use dataclasses or normal classes. #8143
Replies: 42 comments 3 replies
-
@LasseGravesen the problem is that GET operations don't have a body: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET The Pydantic body you are defining in the first example is not equivalent to the query parameters in the second. That Pydantic model would require a JSON body with something like: {
"dt": "2019-06-20T01:02:03+05:00",
"to_sum": [2, 3, 4]
} But what you can do, to avoid having to put all the parameters in each path operation, is to create a dependency, it would require the query parameters as you have them, and then you can put them in a Pydantic model with the shape you want, and it would return that Pydantic model. Or even shorter, you can create a class dependency, check a very similar example in the docs: https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/ |
Beta Was this translation helpful? Give feedback.
-
@tiangolo https://github.com/marshmallow-code/webargs/blob/dev/examples/flask_example.py#L23-L34 That is not a body, but rather it puts the GET query parameters into a model. I'll look into dependencies, but I hope you'll consider the above. |
Beta Was this translation helpful? Give feedback.
-
Dependencies might work, is there any reason why we couldn't use a pydantic class as a dependency? |
Beta Was this translation helpful? Give feedback.
-
@LasseGravesen I think you may have misunderstood the get parameter. why it not use a pydantic class as a dependency? I think that:
Let's think about it. if use pydantic class, we should define the model. maybe like this:
ok, all the parameters are defined in the model. However, In practice, you will find that one request only need two field, such as page and pageSize, Supposing the So if I request the So I must remove the field from the model. There are many more examples.
To make a long story short, I think the code design style is better than use the |
Beta Was this translation helpful? Give feedback.
-
@tiangolo, I had an opportunity to fiddle a little bit more with this today. You can run it using this command I essence: dataclass classes work if I specify Pydantic classes do not work, at least in terms of the generated docs, it just says |
Beta Was this translation helpful? Give feedback.
-
Thanks for the help here guys! @LasseGravesen a Pydantic model declares a JSON object, like a But for query parameters you need independent query parameters, each one with its own type. Maybe an example can help to clarify it, imagine you have a POST endpoint that receives a body with a user, it contains a How would you declare that As part of the model for the user? ...then it would be required as part of the JSON body. What if you had |
Beta Was this translation helpful? Give feedback.
-
@tiangolo I would declare demo and token as a separate parameters for the endpoint, as they seem to me to be separate from the main request information(i.e. the user). In this case you could do either:
If I really wanted to include the extra information in the "model", I would do it like this, which does not actually work right now, in that it just assumes everything is part of the body:
As an aside, you can actually do something like that with dataclasses, though it feels hacky because now the user model contains extra parameters that dont really belong to it.
You could also as you state use two separate models, one for the User and other for extra parameters, which actually works nicely if you use dataclasses, though you must use Depends.
But anyway, the use case I had was that I wanted a single way to define a lot of disparate arguments in a single model like webargs allows you to do, for all types of endpoints, i.e. the definition for body is defined in the same way as query params or url params. So the thought would be to have something like this:
This doesnt work for a pydantic model, in that you just get All this code can be found here: https://gist.github.com/LasseGravesen/8f5c2f510aa419592bf4fe4568db0ae2 |
Beta Was this translation helpful? Give feedback.
-
@LasseGravesen You would do it like this: from fastapi import FastAPI, Depends, Query
app = FastAPI()
class SearchArgs:
def __init__(
self,
query: str = Query(...),
limit: int = Query(10),
offset: int = Query(0),
sort: str = Query("date"),
):
self.query = query
self.limit = limit
self.offset = offset
self.sort = sort
@app.get("/api/v1/search_dataclass", tags=["basic"])
def search(args: SearchArgs = Depends()):
return {"detail": "search-result", "args": args, "results": {"abc": "def"}} Check the docs here: https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/ |
Beta Was this translation helpful? Give feedback.
-
Either of these work for me:
My preference would be to have pydantic as an option as well so I can be consistent with defining arguments with a single approach instead of mixing approaches, which I guess is my main point of contention now. Thanks for leading me to Depends, it's good enough for now, though like I said my preference would be to have pydantic work as well for this use case also. |
Beta Was this translation helpful? Give feedback.
-
@LasseGravesen I was actually looking at the same use case. I get your point @tiangolo for why you might not what to use a pydantic model to define query parameters. However, for my use case, having that option would improve reusability a lot. |
Beta Was this translation helpful? Give feedback.
-
I was setting up some GET and POST routes in FastAPI and I agree with the others here that the pydantic models for the next release of FastAPI should be expanded to include GET requests. For a while I was wondering what was broken about the models I was creating and came across this thread. Any thoughts of adding this? |
Beta Was this translation helpful? Give feedback.
-
With how tight integration of FastApi with Pydantic is, I too had expected to be able to use Pydantic models as query params using Depends(). Now I have duplicate code because of this: @dataclass
class DatacenterNaturalKey:
id: int = Path(..., description="Datacenter id of object")
region: enums.Region = Path(
..., description="Region language code from which this object is"
)
patch: int = Path(..., description="Datacenter version number")
class DatacenterModel(Model):
id: int = Schema(..., description="Datacenter id of object")
region: enums.Region = Schema(
..., description="Region language code from which this object is"
)
patch: int = Schema(..., description="Datacenter version number")
... #rest of schema Since Path is just an extension of Schema, I expected it to be able to declare NaturalKey as Pydantic model, and just extend it in DatacenterModel in addition to my base class Model and leave out these 3 fields. (which does work, but then I can't use NaturalKey as query params - which is the whole purpose of the class...) |
Beta Was this translation helpful? Give feedback.
-
@kuko0411 Instead of copying the code, you might be able to do: DatacenterModel = DatacenterNaturalKey.__pydantic_model__ If you don't like what that does to your IDE/mypy, you can do something like: if typing.TYPE_CHECKING:
DatacenterModel = DatacenterNaturalKey
else:
DatacenterModel = DatacenterNaturalKey.__pydantic_model__ (this isn't quite right, but it may be close enough to be useful.) Given your design, I think this would require some extra massaging to get it to work exactly how you want, but it might be a useful starting point. There is a relatively good reason for why this is hard to achieve with pydantic, and that is because FastAPI dependencies are built by reading the object signature. For a variety of reasons, Pydantic models don't have a signature that is compatible with fastapi dependencies. Even if we modified fastapi to handle pydantic models as a special case, it could still be hard to cover all edge cases due to things like the use of field aliases. If someone really wants this, it shouldn't be too hard to start implementing as a PR -- take a look at |
Beta Was this translation helpful? Give feedback.
-
This is an older issue but I wanted to show my solution to this problem:
I don't know if this helps anyone / solves the problem but it does allow you to use pydantic validation for query parameters and get similar error responses to payload validation failure. |
Beta Was this translation helpful? Give feedback.
-
Hi! class MyQueryParams(BaseModel):
limit: int = Query(0, le=50, description='description')
offset: int = Query(0, le=50)
sort_by: str = Query(None, max_length=50)
direction: str = Query(None, max_length=50)
@validator('direction')
def check_direction(cls, v):
assert v in ('desc', 'asc'), 'wrong direction'
return v
@validator('sort_by')
def check_sort_by(cls, v):
assert v in ModelInDB.__fields__.keys(), 'must refer to DB-representation'
return v
@router.get("/", response_model=MyResponse)
async def get_values(params: MyQueryParams = Depends(MyQueryParams)):
pass This solution doesn't work properly. What is the best practice to realize such case? In documents I've found only an example with ordinary python class without any custom validators. |
Beta Was this translation helpful? Give feedback.
-
Hope this helps someone |
Beta Was this translation helpful? Give feedback.
-
This should be looked at using the deepobject serialisation in openapi and supported just using Query out of the box - I've got another issue open on this that might be worth linking up |
Beta Was this translation helpful? Give feedback.
-
I really want this feature, and implementing this feature leads to consistent behavior with FastAPI already has multiple body parameters feature using {
"skip": 0,
"limit": 30
} to @app.put("/")
async def test(skip: int = Body(..., embed=True), limit: int = Body(..., embed=True)):
... will give you parameter class Filter(BaseModel):
skip: int = None
limit: int = None
@app.put("/")
async def test(filter: Filter = Body(..., embed=False)):
... (Note on both cases, The same way, we should add @router.get("/")
def test(skip: int = Query(..., embed=True), limit: int = Query(..., embed=True)):
pass and @router.get("/")
def test(filter: Filter = Query(..., embed=False)):
pass And to ensure backward compatibility, make |
Beta Was this translation helpful? Give feedback.
-
In the meantime you can use https://github.com/ghandic/FastAPI-deepObject |
Beta Was this translation helpful? Give feedback.
-
Why I want this is that, I want to declare time span with two query parameter class Timespan(BaseModel):
start: datetime
end: datetime
@root_validator
def check_timespan(cls, values):
if values.get('start') > values.get('end'):
raise NegativeTimespanError(start_field="start", end_field="end") # My custom error
return values But currently, there is no way to use this pydantic model in query parameter. |
Beta Was this translation helpful? Give feedback.
-
You can get your model 'exploded' into query params easily enough:
and that will validate the individual query params as per their specific constraints. You'll also see them in the openapi spec as individual query params for the path (including constraint into) - at least in 0.71.0 where I am facing this issue What you cannot currently do is root validators - in this case the error you raise will not be trapped inside fastapi.dependencies.utils.py::solve_dependencies, call to run_in_threadpool ~L527. and will instead be reported back to a normal client as a HTTP:500 error. The TestClient doesn't even see this, it just sees the errors raised when you invoke testclient.get("/") (rather than getting a response with status_code == 500). So for the time being your only option might be to replace your root validator method with just a plain method that inspects 'self' and raises the NegativeTimespanError (for this you can add a handler or extend from HTTPError). |
Beta Was this translation helpful? Give feedback.
-
An update on my above - raising HTTPException inside your root validator seems to work, but you'll need to craft you |
Beta Was this translation helpful? Give feedback.
-
I didn't know I think raising |
Beta Was this translation helpful? Give feedback.
-
@silane I think I worked from https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/ to get as far as using a single model to encapsulate multiple query params into a reusable model. You're right about raising HTTPException, it doesn't seem right but that's the only way I could get it to work. I guess I could dig through the source again an try to submit a PR to address this + update the docs at the same time. |
Beta Was this translation helpful? Give feedback.
-
Oh, I would really appreciate if you could do that!! |
Beta Was this translation helpful? Give feedback.
-
Just got this saucy snippet working, it's based on a previous comment but I got it working with way less boilerplate and it seems to work perfectly. class QueryModel(BaseModel):
def __init__(self, **kwargs):
try:
super().__init__(**kwargs)
except ValidationError as e:
errors = e.errors()
for error in errors:
error["loc"] = ("query",) + error["loc"]
raise HTTPException(422, detail=errors)
# just inherit from QueryModel and use it like a normal pydantic model
class VideoQuery(QueryModel):
project: ProjectType
video: str
@validator("video")
def valid_video(cls, value: str) -> str:
if value != "HelloWorld.mp4":
raise ValueError("video does not exist")
return value
@app.get("/videos/{video}")
def get_video(params: VideoQuery = Depends(VideoQuery)):
pass |
Beta Was this translation helpful? Give feedback.
-
Curious, how does it look in swagger? |
Beta Was this translation helpful? Give feedback.
-
Maybe somewhat surprisingly it works correctly AFAIK and shows the correct parameter names and types. |
Beta Was this translation helpful? Give feedback.
-
I have tried most of the above solutions including class-dependencies, dataclass, overriding depends() and so on. All of them seem to be working fine but none of them meets my requirement. I want to validate query parameters and provide combined error messages to the user but it is not happening ion a particular scenario when errors get caught in pydantic(inbuilt) validation as well as manual/customised validation. For ex- Pydantic model or our dataclass expects user to provide id as integer and date in a specific format(YYYY0MM-DD). We have added manual validation method(@validator) to validate date but not for id and user by mistake provides an alphanumeric ID. In this scenario, I am able to show error only for id["Invalid id"] but not able to validate date because program execution stops immediately. I am wondering if there is a way to validate all the query parameters and provide a combined error messages("invalid id", "invalid date") to the client so that user can correct all of his inputs in one go? |
Beta Was this translation helpful? Give feedback.
-
Sort of, with my method above FastAPI, will first perform pydantic validation on the types e.g. in my example it will ensure |
Beta Was this translation helpful? Give feedback.
-
Description
Is there a way to use pydantic models for GET requests? I would like to have a similar interface for both query params and for the body. So for instance, an example could look like this:
Where as, right now I think you would have to do something like this:
Hope this can be clarified.
Beta Was this translation helpful? Give feedback.
All reactions