Browse Source

rm being able to determine the input format of a model

pull/13464/head
sneakers-the-rat 11 months ago
parent
commit
d3ccab4948
No known key found for this signature in database GPG Key ID: 6DCB96EF1E4D232D
  1. 29
      docs/en/docs/tutorial/request-form-models.md
  2. 21
      docs_src/request_form_models/tutorial004.py
  3. 17
      docs_src/request_form_models/tutorial004_an_py39.py
  4. 9
      docs_src/request_form_models/tutorial004_pv1.py
  5. 9
      docs_src/request_form_models/tutorial004_pv1_an_py39.py
  6. 4
      fastapi/_compat.py

29
docs/en/docs/tutorial/request-form-models.md

@ -107,33 +107,34 @@ and [the field will be omitted altogether if unchecked](https://developer.mozill
When dealing with form models with defaults, When dealing with form models with defaults,
we need to take special care to handle cases where the field being *unset* has a specific meaning. we need to take special care to handle cases where the field being *unset* has a specific meaning.
We also don't want to just treat any time the value is unset as ``False`` -
that would defeat the purpose of the default!
We want to specifically correct the behavior when it is used in the context of a *form.*
In some cases, we can resolve the problem by changing or removing the default, In some cases, we can resolve the problem by changing or removing the default,
but we don't always have that option - but we don't always have that option -
particularly when the model is used in other places than the form particularly when the model is used in other places than the form
(model reuse is one of the benefits of building FastAPI on top of pydantic, after all!). (model reuse is one of the benefits of building FastAPI on top of pydantic, after all!).
To do this, you can use a [`model_validator`](https://docs.pydantic.dev/latest/concepts/validators/#model-validators) The recommended approach, however, is to duplicate your model:
in the `before` mode - before the defaults from the model are applied,
to differentiate between an explicit `False` value and an unset value.
We also don't want to just treat any time the value is unset as ``False`` -
that would defeat the purpose of the default!
We want to specifically correct the behavior when it is used in the context of a *form.*
So we can additionally use the `'fastapi_field'` passed to the
[validation context](https://docs.pydantic.dev/latest/concepts/validators/#validation-context)
to determine whether our model is being validated from form input.
/// note /// note
Validation context is a pydantic v2 only feature! Take care to ensure that your duplicate models don't diverge,
particularly if you are using sqlmodel,
where you will end up with `MyModel`, `MyModelCreate`, and `MyModelCreateForm`.
Also remember to make sure that anywhere else you use this model adds the appropriate
switching logic and static type annotations to accommodate receiving both the original class
and the model we make as a workaround for form handling.
/// ///
{* ../../docs_src/request_form_models/tutorial004_an_py39.py hl[7,13:25] *} {* ../../docs_src/request_form_models/tutorial004_an_py39.py hl[7,13:25] *}
And with that, our form model should behave as expected when it is used with a form, And with that, one of our models should behave as expected when used with a form,
JSON input, or elsewhere in the program! and you can switch between it and other permutations of the model for JSON input
or any other format you may need to handle!
## Summary ## Summary

21
docs_src/request_form_models/tutorial004.py

@ -1,24 +1,20 @@
from typing import cast
from fastapi import FastAPI, Form, Request from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from jinja2 import DictLoader, Environment from jinja2 import DictLoader, Environment
from pydantic import BaseModel, ValidationInfo, model_validator from pydantic import BaseModel, model_validator
class MyModel(BaseModel): class MyModel(BaseModel):
checkbox: bool = True checkbox: bool = True
class MyModelForm(MyModel):
@model_validator(mode="before") @model_validator(mode="before")
def handle_defaults(cls, value: dict, info: ValidationInfo) -> dict: def handle_defaults(cls, value: dict) -> dict:
# if this model is being used outside of fastapi, return normally if "checkbox" not in value:
if info.context is None or "fastapi_field" not in info.context:
return value
# check if we are being validated from form input,
# and if so, treat the unset checkbox as False
field_info = info.context["fastapi_field"].field_info
is_form = type(field_info).__name__ == "Form"
if is_form and "checkbox" not in value:
value["checkbox"] = False value["checkbox"] = False
return value return value
@ -56,5 +52,6 @@ async def show_form(request: Request):
@app.post("/form") @app.post("/form")
async def submit_form(data: MyModel = Form()) -> MyModel: async def submit_form(data: MyModelForm = Form()) -> MyModel:
data = cast(MyModel, data)
return data return data

17
docs_src/request_form_models/tutorial004_an_py39.py

@ -1,4 +1,4 @@
from typing import Annotated from typing import Annotated, cast
from fastapi import FastAPI, Form, Request from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
@ -10,17 +10,11 @@ from pydantic import BaseModel, ValidationInfo, model_validator
class MyModel(BaseModel): class MyModel(BaseModel):
checkbox: bool = True checkbox: bool = True
class MyModelForm(MyModel):
@model_validator(mode="before") @model_validator(mode="before")
def handle_defaults(cls, value: dict, info: ValidationInfo) -> dict: def handle_defaults(cls, value: dict, info: ValidationInfo) -> dict:
# if this model is being used outside of fastapi, return normally if "checkbox" not in value:
if info.context is None or "fastapi_field" not in info.context:
return value
# check if we are being validated from form input,
# and if so, treat the unset checkbox as False
field_info = info.context["fastapi_field"].field_info
is_form = type(field_info).__name__ == "Form"
if is_form and "checkbox" not in value:
value["checkbox"] = False value["checkbox"] = False
return value return value
@ -58,5 +52,6 @@ async def show_form(request: Request):
@app.post("/form") @app.post("/form")
async def submit_form(data: Annotated[MyModel, Form()]) -> MyModel: async def submit_form(data: Annotated[MyModelForm, Form()]) -> MyModel:
data = cast(MyModel, data)
return data return data

9
docs_src/request_form_models/tutorial004_pv1.py

@ -1,3 +1,5 @@
from typing import cast
from fastapi import FastAPI, Form, Request from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@ -8,10 +10,10 @@ from pydantic import BaseModel, root_validator
class MyModel(BaseModel): class MyModel(BaseModel):
checkbox: bool = True checkbox: bool = True
class MyModelForm(BaseModel):
@root_validator(pre=True) @root_validator(pre=True)
def handle_defaults(cls, value: dict) -> dict: def handle_defaults(cls, value: dict) -> dict:
# We can't tell if we're being validated by fastAPI,
# so we have to just YOLO this.
if "checkbox" not in value: if "checkbox" not in value:
value["checkbox"] = False value["checkbox"] = False
return value return value
@ -50,5 +52,6 @@ async def show_form(request: Request):
@app.post("/form") @app.post("/form")
async def submit_form(data: MyModel = Form()) -> MyModel: async def submit_form(data: MyModelForm = Form()) -> MyModel:
data = cast(MyModel, data)
return data return data

9
docs_src/request_form_models/tutorial004_pv1_an_py39.py

@ -1,4 +1,4 @@
from typing import Annotated from typing import Annotated, cast
from fastapi import FastAPI, Form, Request from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
@ -10,10 +10,10 @@ from pydantic import BaseModel, root_validator
class MyModel(BaseModel): class MyModel(BaseModel):
checkbox: bool = True checkbox: bool = True
class MyModelForm(MyModel):
@root_validator(pre=True) @root_validator(pre=True)
def handle_defaults(cls, value: dict) -> dict: def handle_defaults(cls, value: dict) -> dict:
# We can't tell if we're being validated by fastAPI,
# so we have to just YOLO this.
if "checkbox" not in value: if "checkbox" not in value:
value["checkbox"] = False value["checkbox"] = False
return value return value
@ -52,5 +52,6 @@ async def show_form(request: Request):
@app.post("/form") @app.post("/form")
async def submit_form(data: Annotated[MyModel, Form()]) -> MyModel: async def submit_form(data: Annotated[MyModelForm, Form()]) -> MyModel:
data = cast(MyModel, data)
return data return data

4
fastapi/_compat.py

@ -126,9 +126,7 @@ if PYDANTIC_V2:
) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
try: try:
return ( return (
self._type_adapter.validate_python( self._type_adapter.validate_python(value, from_attributes=True),
value, from_attributes=True, context={"fastapi_field": self}
),
None, None,
) )
except ValidationError as exc: except ValidationError as exc:

Loading…
Cancel
Save