fastapi
fastapi copied to clipboard
[FEATURE] Use pydantic's BaseModel as dependency with validators
Is your feature request related to a problem
I wish to use pydantic's BaseModel with validators as depency injection. Classes as dependencies are handy because I can fuse multiple path/body/... parameters into a single object, but what would be cool is to be able to check those parameters based on other parameters via pydantic's validators even if those values are transferred in different forms (path/body/...).
The solution you would like
So, what already works is something like this:
class User(BaseModel):
name: str
password: str
age: int = Query(None)
@app.get('/users/{name}/{password}')
async def fetch(user: User = Depends()):
return user
This correctly checks types and benefits from the usual genius automatic error and documentation generation. Though, adding validators to e.g. make sure the user's name is not the same as her password leads to an internal server error when the ValueError
is raised.
class User(BaseModel):
name: str
password: str
age: int = Query(None)
@validator('password')
def check_not_equal(cls, v, values):
if v == values['name']:
raise ValueError('name and password match')
return v
@app.get('/users/{name}/{password}')
async def fetch(user: User = Depends()):
return user
The optimal thing would be that those validators work even when used inside a dependency. The main benefit is that this would allow me to validate values based on other values even if those values are transferred in different forms (path/body/...).
Describe alternatives you've considered
The alternative is to not do the validation in the dependency, but afterwards.
Additional context
Thank you for your work on this framework, FastAPI is super cool! :sunglasses:
Yep, ran into this on the very first project I implemented.
Can you watch this? Perhaps it could be better, I just started to deal with this framework.
class UserRequest(BaseModel):
name: str
password: str
age: int = None
@validator('password')
def check_not_equal(cls, v, values):
if v == values['name']:
raise ValueError('name and password match')
return v
@classmethod
async def depends(
cls,
password: str,
name: str = Query(..., description='name description', example='Leo'),
age: int = Query(None)
):
try:
return cls(name=name, password=password, age=age)
except ValidationError as e:
for error in e.errors():
error['loc'] = ['query'] + list(error['loc'])
raise HTTPException(422, detail=e.errors())
@app.get('/data')
async def fetch(user_request: UserRequest = Depends(UserRequest.depends)):
return user_request
Can you watch this? Perhaps it could be better, I just started to deal with this framework.
Below seems to also work when using Pydantic dataclass. But you need to manually construct the FastAPIs RequestValidationError
exception for the custom validator (the fields data types are validated just fine):
@pydantic.dataclasses.dataclass
class UserRequest:
name: str = Query(..., description='name description', example='Leo')
password: str = Query(..., description='password description')
age: Optional[int] = Query(None)
def __post_init_post_parse__(self) -> None:
if self.password == self.name:
raise RequestValidationError([ErrorWrapper('name and password match', 'query.password')])
@app.get('/data')
async def fetch(user_request: UserRequest = Depends()):
return user_request
Above also produces correct OpenAPI schema.
I liked @masksshow's solution, but disliked the idea of duplicating the code signature of each model. I've automated their solution with this helper function, which accepts a class and returns a function with the same signature as the class. The wrapped function attempts to init and return instance of the class, and if ValidationErrors are caught it handles them as per @masksshow's solution.
from inspect import signature
from fastapi.exceptions import ValidationError, HTTPException
def make_dependable(cls):
"""
Pydantic BaseModels are very powerful because we get lots of validations and type checking right out of the box.
FastAPI can accept a BaseModel as a route Dependency and it will automatically handle things like documentation
and error handling. However, if we define custom validators then the errors they raise are not handled, leading
to HTTP 500's being returned.
To better understand this issue, you can visit https://github.com/tiangolo/fastapi/issues/1474 for context.
A workaround proposed there adds a classmethod which attempts to init the BaseModel and handles formatting of
any raised ValidationErrors, custom or otherwise. However, this means essentially duplicating the class's
signature. This function automates the creation of a workaround method with a matching signature so that you
can avoid code duplication.
usage:
async def fetch(thing_request: ThingRequest = Depends(make_dependable(ThingRequest))):
"""
def init_cls_and_handle_errors(*args, **kwargs):
try:
signature(init_cls_and_handle_errors).bind(*args, **kwargs)
return cls(*args, **kwargs)
except ValidationError as e:
for error in e.errors():
error['loc'] = ['query'] + list(error['loc'])
raise HTTPException(422, detail=e.errors())
init_cls_and_handle_errors.__signature__ = signature(cls)
return init_cls_and_handle_errors