From d3ccab49485108978f4221584d73e591a972ef3d Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Tue, 19 Aug 2025 23:38:25 -0700 Subject: [PATCH] rm being able to determine the input format of a model --- docs/en/docs/tutorial/request-form-models.md | 29 ++++++++++--------- docs_src/request_form_models/tutorial004.py | 21 ++++++-------- .../tutorial004_an_py39.py | 17 ++++------- .../request_form_models/tutorial004_pv1.py | 9 ++++-- .../tutorial004_pv1_an_py39.py | 9 +++--- fastapi/_compat.py | 4 +-- 6 files changed, 42 insertions(+), 47 deletions(-) diff --git a/docs/en/docs/tutorial/request-form-models.md b/docs/en/docs/tutorial/request-form-models.md index 277c53ce8..f6446282d 100644 --- a/docs/en/docs/tutorial/request-form-models.md +++ b/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, 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, but we don't always have that option - 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!). -To do this, you can use a [`model_validator`](https://docs.pydantic.dev/latest/concepts/validators/#model-validators) -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. +The recommended approach, however, is to duplicate your model: /// 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] *} -And with that, our form model should behave as expected when it is used with a form, -JSON input, or elsewhere in the program! +And with that, one of our models should behave as expected when used with a form, +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 diff --git a/docs_src/request_form_models/tutorial004.py b/docs_src/request_form_models/tutorial004.py index 1dfc6f80d..57ce0ccf8 100644 --- a/docs_src/request_form_models/tutorial004.py +++ b/docs_src/request_form_models/tutorial004.py @@ -1,24 +1,20 @@ +from typing import cast + from fastapi import FastAPI, Form, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from jinja2 import DictLoader, Environment -from pydantic import BaseModel, ValidationInfo, model_validator +from pydantic import BaseModel, model_validator class MyModel(BaseModel): checkbox: bool = True - @model_validator(mode="before") - def handle_defaults(cls, value: dict, info: ValidationInfo) -> dict: - # if this model is being used outside of fastapi, return normally - 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: +class MyModelForm(MyModel): + @model_validator(mode="before") + def handle_defaults(cls, value: dict) -> dict: + if "checkbox" not in value: value["checkbox"] = False return value @@ -56,5 +52,6 @@ async def show_form(request: Request): @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 diff --git a/docs_src/request_form_models/tutorial004_an_py39.py b/docs_src/request_form_models/tutorial004_an_py39.py index d1d1fb64f..42a65ece2 100644 --- a/docs_src/request_form_models/tutorial004_an_py39.py +++ b/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.responses import HTMLResponse @@ -10,17 +10,11 @@ from pydantic import BaseModel, ValidationInfo, model_validator class MyModel(BaseModel): checkbox: bool = True + +class MyModelForm(MyModel): @model_validator(mode="before") def handle_defaults(cls, value: dict, info: ValidationInfo) -> dict: - # if this model is being used outside of fastapi, return normally - 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: + if "checkbox" not in value: value["checkbox"] = False return value @@ -58,5 +52,6 @@ async def show_form(request: Request): @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 diff --git a/docs_src/request_form_models/tutorial004_pv1.py b/docs_src/request_form_models/tutorial004_pv1.py index 0340d7d7d..69f28aab7 100644 --- a/docs_src/request_form_models/tutorial004_pv1.py +++ b/docs_src/request_form_models/tutorial004_pv1.py @@ -1,3 +1,5 @@ +from typing import cast + from fastapi import FastAPI, Form, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates @@ -8,10 +10,10 @@ from pydantic import BaseModel, root_validator class MyModel(BaseModel): checkbox: bool = True + +class MyModelForm(BaseModel): @root_validator(pre=True) 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: value["checkbox"] = False return value @@ -50,5 +52,6 @@ async def show_form(request: Request): @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 diff --git a/docs_src/request_form_models/tutorial004_pv1_an_py39.py b/docs_src/request_form_models/tutorial004_pv1_an_py39.py index d0b45003f..28bbc7b32 100644 --- a/docs_src/request_form_models/tutorial004_pv1_an_py39.py +++ b/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.responses import HTMLResponse @@ -10,10 +10,10 @@ from pydantic import BaseModel, root_validator class MyModel(BaseModel): checkbox: bool = True + +class MyModelForm(MyModel): @root_validator(pre=True) 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: value["checkbox"] = False return value @@ -52,5 +52,6 @@ async def show_form(request: Request): @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 diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 072aa5658..c07e4a3b0 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -126,9 +126,7 @@ if PYDANTIC_V2: ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: try: return ( - self._type_adapter.validate_python( - value, from_attributes=True, context={"fastapi_field": self} - ), + self._type_adapter.validate_python(value, from_attributes=True), None, ) except ValidationError as exc: