mirror of https://github.com/tiangolo/fastapi.git
docs for handling default values, pass field to validation context
This commit is contained in:
parent
529d486a7b
commit
f63e983b60
|
|
@ -73,6 +73,68 @@ They will receive an error response telling them that the field `extra` is not a
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Default Fields
|
||||||
|
|
||||||
|
Form-encoded data has some quirks that can make working with pydantic models counterintuitive.
|
||||||
|
|
||||||
|
Say, for example, you were generating an HTML form from a model,
|
||||||
|
and that model had a boolean field in it that you wanted to display as a checkbox
|
||||||
|
with a default `True` value:
|
||||||
|
|
||||||
|
{* ../../docs_src/request_form_models/tutorial003_an_py39.py hl[10,18:22] *}
|
||||||
|
|
||||||
|
This works as expected when the checkbox remains checked,
|
||||||
|
the form encoded data in the request looks like this:
|
||||||
|
|
||||||
|
```formencoded
|
||||||
|
checkbox=on
|
||||||
|
```
|
||||||
|
|
||||||
|
and the JSON response is also correct:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"checkbox":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
When the checkbox is *unchecked*, though, something strange happens.
|
||||||
|
The submitted form data is *empty*,
|
||||||
|
and the returned JSON data still shows `checkbox` still being `true`!
|
||||||
|
|
||||||
|
This is because checkboxes in HTML forms don't work exactly like the boolean inputs we expect,
|
||||||
|
when a checkbox is checked, if there is no `value` attribute, the value will be `"on"`,
|
||||||
|
and [the field will be omitted altogether if unchecked](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/checkbox).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
/// note
|
||||||
|
|
||||||
|
Validation context is a pydantic v2 only feature!
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
{* ../../docs_src/request_form_models/tutorial004_an_py39.py hl[3,12:24] *}
|
||||||
|
|
||||||
|
And with that, our form model should behave as expected when it is used with a form,
|
||||||
|
JSON input, or elsewhere in the program!
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
You can use Pydantic models to declare form fields in FastAPI. 😎
|
You can use Pydantic models to declare form fields in FastAPI. 😎
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from fastapi import FastAPI, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from jinja2 import DictLoader, Environment
|
||||||
|
|
||||||
|
class MyModel(BaseModel):
|
||||||
|
checkbox: bool = True
|
||||||
|
|
||||||
|
form_template = """
|
||||||
|
<form action="/form" method="POST">
|
||||||
|
{% for field_name, field in model.model_fields.items() %}
|
||||||
|
<p>
|
||||||
|
<label for="{{ field_name }}">{{ field_name }}</label>
|
||||||
|
{% if field.annotation.__name__ == "bool" %}
|
||||||
|
<input type="checkbox" name="{{field_name}}"
|
||||||
|
{% if field.default %}
|
||||||
|
checked="checked"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
<input name="{{ field_name }}">
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
loader = DictLoader({"form.html": form_template})
|
||||||
|
templates = Jinja2Templates(env=Environment(loader=loader))
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/form", response_class=HTMLResponse)
|
||||||
|
async def show_form(request: Request):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request, name="form.html", context={"model": MyModel}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post('/form')
|
||||||
|
async def submit_form(data: MyModel = Form()) -> MyModel:
|
||||||
|
return data
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from fastapi import FastAPI, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from jinja2 import DictLoader, Environment
|
||||||
|
|
||||||
|
class MyModel(BaseModel):
|
||||||
|
checkbox: bool = True
|
||||||
|
|
||||||
|
form_template = """
|
||||||
|
<form action="/form" method="POST">
|
||||||
|
{% for field_name, field in model.model_fields.items() %}
|
||||||
|
<p>
|
||||||
|
<label for="{{ field_name }}">{{ field_name }}</label>
|
||||||
|
{% if field.annotation.__name__ == "bool" %}
|
||||||
|
<input type="checkbox" name="{{field_name}}"
|
||||||
|
{% if field.default %}
|
||||||
|
checked="checked"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
<input name="{{ field_name }}">
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
loader = DictLoader({"form.html": form_template})
|
||||||
|
templates = Jinja2Templates(env=Environment(loader=loader))
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/form", response_class=HTMLResponse)
|
||||||
|
async def show_form(request: Request):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request, name="form.html", context={"model": MyModel}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post('/form')
|
||||||
|
async def submit_form(data: Annotated[MyModel, Form()]) -> MyModel:
|
||||||
|
return data
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
from pydantic import BaseModel, ValidationInfo, model_validator
|
||||||
|
from fastapi import FastAPI, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from jinja2 import DictLoader, Environment
|
||||||
|
|
||||||
|
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:
|
||||||
|
value['checkbox'] = False
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
form_template = """
|
||||||
|
<form action="/form" method="POST">
|
||||||
|
{% for field_name, field in model.model_fields.items() %}
|
||||||
|
<p>
|
||||||
|
<label for="{{ field_name }}">{{ field_name }}</label>
|
||||||
|
{% if field.annotation.__name__ == "bool" %}
|
||||||
|
<input type="checkbox" name="{{field_name}}"
|
||||||
|
{% if field.default %}
|
||||||
|
checked="checked"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
<input name="{{ field_name }}">
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
loader = DictLoader({"form.html": form_template})
|
||||||
|
templates = Jinja2Templates(env=Environment(loader=loader))
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/form", response_class=HTMLResponse)
|
||||||
|
async def show_form(request: Request):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request, name="form.html", context={"model": MyModel}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post('/form')
|
||||||
|
async def submit_form(data: MyModel = Form()) -> MyModel:
|
||||||
|
return data
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ValidationInfo, model_validator
|
||||||
|
from fastapi import FastAPI, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from jinja2 import DictLoader, Environment
|
||||||
|
|
||||||
|
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:
|
||||||
|
value['checkbox'] = False
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
form_template = """
|
||||||
|
<form action="/form" method="POST">
|
||||||
|
{% for field_name, field in model.model_fields.items() %}
|
||||||
|
<p>
|
||||||
|
<label for="{{ field_name }}">{{ field_name }}</label>
|
||||||
|
{% if field.annotation.__name__ == "bool" %}
|
||||||
|
<input type="checkbox" name="{{field_name}}"
|
||||||
|
{% if field.default %}
|
||||||
|
checked="checked"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
<input name="{{ field_name }}">
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
loader = DictLoader({"form.html": form_template})
|
||||||
|
templates = Jinja2Templates(env=Environment(loader=loader))
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/form", response_class=HTMLResponse)
|
||||||
|
async def show_form(request: Request):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request, name="form.html", context={"model": MyModel}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post('/form')
|
||||||
|
async def submit_form(data: Annotated[MyModel, Form()]) -> MyModel:
|
||||||
|
return data
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
from pydantic import BaseModel, model_validator
|
||||||
|
from fastapi import FastAPI, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from jinja2 import DictLoader, Environment
|
||||||
|
|
||||||
|
class MyModel(BaseModel):
|
||||||
|
checkbox: bool = True
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
form_template = """
|
||||||
|
<form action="/form" method="POST">
|
||||||
|
{% for field_name, field in model.model_fields.items() %}
|
||||||
|
<p>
|
||||||
|
<label for="{{ field_name }}">{{ field_name }}</label>
|
||||||
|
{% if field.annotation.__name__ == "bool" %}
|
||||||
|
<input type="checkbox" name="{{field_name}}"
|
||||||
|
{% if field.default %}
|
||||||
|
checked="checked"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
<input name="{{ field_name }}">
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
loader = DictLoader({"form.html": form_template})
|
||||||
|
templates = Jinja2Templates(env=Environment(loader=loader))
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/form", response_class=HTMLResponse)
|
||||||
|
async def show_form(request: Request):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request, name="form.html", context={"model": MyModel}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post('/form')
|
||||||
|
async def submit_form(data: MyModel = Form()) -> MyModel:
|
||||||
|
return data
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import BaseModel, model_validator
|
||||||
|
from fastapi import FastAPI, Form, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from jinja2 import DictLoader, Environment
|
||||||
|
|
||||||
|
class MyModel(BaseModel):
|
||||||
|
checkbox: bool = True
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
form_template = """
|
||||||
|
<form action="/form" method="POST">
|
||||||
|
{% for field_name, field in model.model_fields.items() %}
|
||||||
|
<p>
|
||||||
|
<label for="{{ field_name }}">{{ field_name }}</label>
|
||||||
|
{% if field.annotation.__name__ == "bool" %}
|
||||||
|
<input type="checkbox" name="{{field_name}}"
|
||||||
|
{% if field.default %}
|
||||||
|
checked="checked"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
<input name="{{ field_name }}">
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
loader = DictLoader({"form.html": form_template})
|
||||||
|
templates = Jinja2Templates(env=Environment(loader=loader))
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/form", response_class=HTMLResponse)
|
||||||
|
async def show_form(request: Request):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request, name="form.html", context={"model": MyModel}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post('/form')
|
||||||
|
async def submit_form(data: Annotated[MyModel, Form()]) -> MyModel:
|
||||||
|
return data
|
||||||
|
|
@ -126,7 +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(value, from_attributes=True),
|
self._type_adapter.validate_python(value, from_attributes=True, context={"fastapi_field": self}),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue