Skip to content

Instantly share code, notes, and snippets.

@fletcheaston
Last active July 5, 2021 19:29
Show Gist options
  • Save fletcheaston/98e2fa088e101d6da30b414c3ba9b9bc to your computer and use it in GitHub Desktop.
Save fletcheaston/98e2fa088e101d6da30b414c3ba9b9bc to your computer and use it in GitHub Desktop.
Pydantic models from FastAPI query parameters and forms.
# Schema input manipulation. Very hacky, but it generates nice docs.
# Copied from https://github.com/tiangolo/fastapi/issues/318#issuecomment-691121286
# These don't work for nested Pydantic models.
import inspect
from types import FunctionType
from typing import Dict, Type
from fastapi import ( # noqa
BackgroundTasks,
Form,
HTTPException,
Query,
Request,
Response,
)
from pydantic import BaseModel, ValidationError # noqa
def as_query(name: str, model_cls: Type[BaseModel]) -> FunctionType:
"""
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
field_name = field_model.name
names.append(field_name)
annotations[field_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
def as_form(name: str, model_cls: Type[BaseModel]) -> FunctionType:
"""
Takes a pydantic model class as input and creates a dependency with corresponding
Form parameter definitions that can be used for POST requests.
This will only work, if the fields defined in the input model can be turned into
suitable form 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
field_name = field_model.name
names.append(field_name)
annotations[field_name] = field_model.outer_type_
defaults.append(Form(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'] = ['form'] + 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
################################################################################
# Example for your FastAPI app.
import uuid
from typing import Optional
from fastapi import Depends, FastAPI
app = FastAPI()
# This model works great.
class QueryModel(BaseModel):
string_param: str = "default argument"
int_param: int
uuid_param: Optional[uuid.UUID]
@app.get("/good-query", response_model=QueryModel)
def test_good_query(good_query: QueryModel = Depends(as_query("good_query", QueryModel))):
return good_query
# This model does not work.
class BadQueryModel(BaseModel):
common_query: QueryModel
other_param: str
# Uncommenting this will give the following error on startup...
# `AssertionError: Param: common_query can only be a request body, using Body(...)`
# @app.get("/bad-query", response_model=BadQueryModel)
# def test_bad_query(bad_query: BadQueryModel = Depends(as_query("bad_query", BadQueryModel))):
# return bad_query
# This model works great.
class FormModel(BaseModel):
string_param: str = "default argument"
int_param: int
uuid_param: Optional[uuid.UUID]
@app.post("/good-form", response_model=FormModel)
def test_good_form(good_form: FormModel = Depends(as_form("good_form", FormModel))):
return good_form
# This model does not work.
class BadFormModel(BaseModel):
common_form: FormModel
other_param: str
# This will NOT give an error on startup, but it will error if you try to use the endpoint.
@app.post("/bad-form", response_model=BadFormModel)
def test_bad_form(bad_form: BadFormModel = Depends(as_form("bad_form", BadFormModel))):
return bad_form
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, log_level="info")
@fletcheaston
Copy link
Author

Edited to include examples for both query and form endpoints, and some small bugs/typos.

The docs render very nicely for the good examples.

The good query endpoint works as expected, you can set Optional types and types with default arguments. The docs are rendered nicely.
good-query

The good form endpoint works as expected, you can set Optional types and types with default arguments. The docs are rendered nicely.
good-form

The bad form endpoint doesn't work as expected, and the docs are... strange. I wasn't able to get the nested common_form object populating correctly.
bad-form

Here's the error message I get from the docs when trying to use the bad form endpoint.
bad-form-response

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment