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:
class PingArgs(BaseModel):
"""Model input for PingArgs."""
dt: datetime.datetime = ...
to_sum: List[int] = ...
@validator("dt", pre=False, always=True, whole=True)
def validate_dt(cls, v, values):
"""Validate dt."""
parsed_dt = v.replace(tzinfo=None)
return parsed_dt
@router.get("/ping", tags=["basic"])
def ping(args: PingArgs, request: Request):
"""Example."""
return JSONResponse(
status_code=starlette.status.HTTP_200_OK,
content={"detail": "pong", "dt": args.dt.isoformat() "summed": sum(x for x in args.to_sum)},
)
Where as, right now I think you would have to do something like this:
@router.get("/ping", tags=["basic"])
def ping(dt: datetime.datetime = Query(None), to_sum: List[int] = Query(None), request: Request):
"""Example."""
parsed_dt = dt.replace(tzinfo=None)
return JSONResponse(
status_code=starlette.status.HTTP_200_OK,
content={"detail": "pong", "dt": dt.isoformat() "summed": sum(x for x in to_sum)},
)
Hope this can be clarified.
@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/
@tiangolo
It would not be a body, it would just be GET parameters as defined in a model. For instance, I come from the perspective of Flask and webargs, where you can define a 'model' and use that with the same interface for both GET and POST parameters, see for instance here:
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.
Dependencies might work, is there any reason why we couldn't use a pydantic class as a dependency?
@LasseGravesen I think you may have misunderstood the get parameter.
why it not use a pydantic class as a dependency?
I think that:
http://www.localhost.com?q=1&page=1&pageSize=1&field='name'
Let's think about it. if use pydantic class, we should define the model. maybe like this:
class QueryArgs(BaseModel):
q: int = ...
page: int = ...
pageSize: int = ...
field: string = ...
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 page range is 1-100, pageSize range is 1 - 100.
So if I request the pageSize is -1, it will throw error. however the “-1 ” represents no need for paging.
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 pydantic model and more suitable for get parameters.
@tiangolo, I had an opportunity to fiddle a little bit more with this today.
Here is a full example of the different ways to specify parameters using depends: https://gist.github.com/Atheuz/075f4d8fe3b56d034741301ba2574ef1
You can run it using this command uvicorn run:app --reload --host 0.0.0.0 --port 8080
I essence: dataclass classes work if I specify Query(None) and Depends, pure classes work if I specify Query(None) and use Depends, and input parameters in the signature just works.
Pydantic classes do not work, at least in terms of the generated docs, it just says data instead of the expected dt and to_sum.
dataclasses in the generated docs:

pydantic in the generated docs:

Thanks for the help here guys!
@LasseGravesen a Pydantic model declares a JSON object, like a dict.
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 username and age. But the same endpoint also receives a query parameter demo that tells the API to only test if it would work, but not save the user.
How would you declare that demo in a model?
As part of the model for the user? ...then it would be required as part of the JSON body.
As another model? ...then it would be interpreted as a two-model composite body.
What if you had demo in a Pydantic model and then decided to add another query parameter token? How would FastAPI know that demo, inside a model, is a query parameter, but token, not in a model, is also a query parameter?
@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:
class UserPydantic(BaseModel):
username: str = ...
age: int = ...
@app.post("/api/v1/ping_pydantic", tags=["basic"])
def ping_pydantic(user: UserPydantic, demo: bool = Query(False), token: str = Query(None)):
"""Args defined in a pydantic model."""
return JSONResponse(
status_code=starlette.status.HTTP_200_OK,
content={"detail": "pong", "user": user.dict()},
)
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:
class UserPydantic2(BaseModel):
username: str = ...
age: int = ...
demo: bool = Query(False)
token: str = Query(None)
@app.post("/api/v1/ping_pydantic2", tags=["basic"])
def ping_pydantic2(user: UserPydantic2):
"""Args defined in a pydantic model."""
return JSONResponse(
status_code=starlette.status.HTTP_200_OK,
content={"detail": "pong", "user": user.dict()},
)
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.
@dataclass
class UserDataclass:
username: str = Body(None)
age: int = Body(None)
demo: bool = Query(False)
token: str = Query(None)
@app.post("/api/v1/user_dataclass", tags=["basic"])
def ping_dataclass(user: UserDataclass = Depends(UserDataclass)):
"""Args defined in a dataclass. Kinda broken."""
return JSONResponse(
status_code=starlette.status.HTTP_200_OK,
content={"detail": "pong", "user": asdict(user)},
)
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.
@dataclass
class UserDataclass2:
username: str = Body(None)
age: int = Body(None)
@dataclass
class ExtraParams:
demo: bool = Query(False)
token: str = Query(None)
@app.post("/api/v1/user_dataclass2", tags=["basic"])
def ping_dataclass2(user: UserDataclass2 = Depends(UserDataclass2), extra_params: ExtraParams = Depends(ExtraParams)):
"""Args defined in two models."""
return JSONResponse(
status_code=starlette.status.HTTP_200_OK,
content={"detail": "pong", "user": asdict(user)},
)
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:
@dataclass
class SearchArgs:
query: str = Query(...)
limit: int = Query(10)
offset: int = Query(0)
sort: str = Query("date")
@app.get("/api/v1/search_dataclass", tags=["basic"])
def search(args: SearchArgs = Depends(SearchArgs)):
return JSONResponse(
status_code=starlette.status.HTTP_200_OK,
content={"detail": "search-result", "args": asdict(args), "results": {"abc": "def"}}
)
This doesnt work for a pydantic model, in that you just get data in the generated docs.
All this code can be found here: https://gist.github.com/LasseGravesen/8f5c2f510aa419592bf4fe4568db0ae2
@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/
@tiangolo
Either of these work for me:
class SearchArgs1234:
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_class", tags=["basic"])
def search(args: SearchArgs1234 = Depends()):
return {"detail": "search-result", "args": args, "results": {"abc": "def"}}
@dataclass
class SearchArgs123:
query: str = Query(...)
limit: int = Query(10)
offset: int = Query(0)
sort: int = Query("date")
@app.get("/api/v1/search_dataclass", tags=["basic"])
def search123(args: SearchArgs123 = Depends(SearchArgs123)):
return {"detail": "search-result", "args": args, "results": {"abc": "def"}}
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.
@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.
@tiangolo
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?
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...)
@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 get_typed_signature in https://github.com/tiangolo/fastapi/pull/451/files. You'd just need to add special handling for pydantic models based on the model.__fields__. I just don't know how hard it will be to handle all edge cases cleanly.
This is an older issue but I wanted to show my solution to this problem:
class PagingQuery(BaseModel):
page: conint(ge=1)
page_size: conint(ge=1, le=250) = 50
@classmethod
async def depends(cls, page: int = 1, page_size: int = 50):
try:
return cls(page=page, page_size=page_size)
except ValidationError as e:
errors = e.errors()
for error in errors:
error['loc'] = ['query'] + list(error['loc'])
raise HTTPException(422, detail=errors)
@app.get("/example", tags=["basic"])
def example(paging: PagingQuery = Depends(PagingQuery.depends)):
return {"page": paging.page, "page_size": paging.page_size}
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.
Hi!
FastAPI + Pydantic present a really cool duo, but I couldn't find one (very natural, in my opinion) feature:
Is there a way to declare query parameters as Pydantic model with all advantages of validators and type checking? For example:
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.
@myraygunbarrel if you drop the validator calls and make use of __post_init__ to perform those checks instead (and manually handle raising a 422), you can just use a vanilla dataclass with everything else the same. You'll still get pydantic-powered parsing/type-checking because of the way FastAPI handles dependencies.
I haven't tested it and I'm not 100% confident, but I also think you could keep everything the same (including your custom validators) by making use of a pydantic dataclass:
from pydantic.dataclasses import dataclass
@dataclass
class MyQueryParams:
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
If that doesn't work and you can't easily get it to work, share the traceback and maybe we can figure it out.
@dmontagu Thank you. assert statement doesn't work as expected (application throws an Internal Server Error), but if explicitly rise HTTPException, it works pretty fine:
@dataclass
class MyQueryParams:
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):
if v not in ('desc', 'asc'):
raise HTTPException(status_code=422,
detail='wrong direction')
return v
@validator('sort_by')
def check_sort_by(cls, v):
if v not in ModelInDB.__fields__.keys():
raise HTTPException(status_code=422,
detail='sort_by must refer to DB-representation')
return v
@router.get("/", response_model=MyResponse)
async def get_values(params: MyQueryParams = Depends(MyQueryParams)):
pass
But It would be very convenient if ValidationError had the same look and behavior around an app.
Thanks everyone for the discussion here!
Having a single Pydantic model for query parameters could be interpreted as:
http://somedomain.com/?args={"k1":"v1","k2":"v2"}
or
http://somedomain.com/?k1=v1&k2=v2
or many other alternatives...
And we are not even discussing sub-models, that are valid in Pydantic models (and request bodies) but the behavior would be undefined for non-body parameters (query, path, etc).
There's no obvious way to go about how it would be interpreted that works for all the cases (including other people's use cases, future use cases, etc).
So it doesn't really make sense to have it in FastAPI for a custom use case as it's very subjective and dependent on the conventions of the team.
I think several of the other use cases/ideas are not really the same as the original comment, so I'm gonna close this issue now and we can discuss the rest in other issues. So, feel free to create new issues for the other use cases (or add more comments here).
This is an older issue but I wanted to show my solution to this problem:
class PagingQuery(BaseModel): page: conint(ge=1) page_size: conint(ge=1, le=250) = 50 @classmethod async def depends(cls, page: int = 1, page_size: int = 50): try: return cls(page=page, page_size=page_size) except ValidationError as e: errors = e.errors() for error in errors: error['loc'] = ['query'] + list(error['loc']) raise HTTPException(422, detail=errors) @app.get("/example", tags=["basic"]) def example(paging: PagingQuery = Depends(PagingQuery.depends)): return {"page": paging.page, "page_size": paging.page_size}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.
This answer feels good to me, thank you!
To expand on the @jimcarreer solution (thx). If you need this a lot, you might want to generate the depends functions. This also copies any descriptions from fields with Body() in the model to Query() defaults in the dependency:
from typing import Dict
from types import FunctionType
import inspect
from fastapi import Query, HTTPException # pylint: disable=unused-import
from pydantic import ValidationError # pylint: disable=unused-import
def parameter_dependency_from_model(name: str, model_cls):
'''
Takes a pydantic model class as input and creates a dependency with corresponding
Query parameter definitions that can be used for GET
requests.
This will only work, if the fields defined in the input model can be turned into
suitable query parameters. Otherwise fastapi will complain down the road.
Arguments:
name: Name for the dependency function.
model_cls: A ``BaseModel`` inheriting model class as input.
'''
names = []
annotations: Dict[str, type] = {}
defaults = []
for field_model in model_cls.__fields__.values():
field_info = field_model.field_info
names.append(field_model.name)
annotations[field_model.name] = field_model.outer_type_
defaults.append(Query(field_model.default, description=field_info.description))
code = inspect.cleandoc('''
def %s(%s):
try:
return %s(%s)
except ValidationError as e:
errors = e.errors()
for error in errors:
error['loc'] = ['query'] + list(error['loc'])
raise HTTPException(422, detail=errors)
''' % (
name, ', '.join(names), model_cls.__name__,
', '.join(['%s=%s' % (name, name) for name in names])))
compiled = compile(code, 'string', 'exec')
env = {model_cls.__name__: model_cls}
env.update(**globals())
func = FunctionType(compiled.co_consts[0], env, name)
func.__annotations__ = annotations
func.__defaults__ = (*defaults,)
return func
Not perfect, but might be a start.
Most helpful comment
@LasseGravesen You would do it like this:
Check the docs here: https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/