Skip to content

Commit 4633b1b

Browse files
tiangolosvlandeg
andauthored
✨ Add support for forbidding extra form fields with Pydantic models (#12134)
Co-authored-by: Sofie Van Landeghem <svlandeg@users.noreply.github.com>
1 parent 1b06b53 commit 4633b1b

File tree

14 files changed

+1360
-3
lines changed

14 files changed

+1360
-3
lines changed

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

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Form Models
22

3-
You can use Pydantic models to declare form fields in FastAPI.
3+
You can use **Pydantic models** to declare **form fields** in FastAPI.
44

55
/// info
66

@@ -22,7 +22,7 @@ This is supported since FastAPI version `0.113.0`. 🤓
2222

2323
## Pydantic Models for Forms
2424

25-
You just need to declare a Pydantic model with the fields you want to receive as form fields, and then declare the parameter as `Form`:
25+
You just need to declare a **Pydantic model** with the fields you want to receive as **form fields**, and then declare the parameter as `Form`:
2626

2727
//// tab | Python 3.9+
2828

@@ -54,7 +54,7 @@ Prefer to use the `Annotated` version if possible.
5454

5555
////
5656

57-
FastAPI will extract the data for each field from the form data in the request and give you the Pydantic model you defined.
57+
**FastAPI** will **extract** the data for **each field** from the **form data** in the request and give you the Pydantic model you defined.
5858

5959
## Check the Docs
6060

@@ -63,3 +63,72 @@ You can verify it in the docs UI at `/docs`:
6363
<div class="screenshot">
6464
<img src="/img/tutorial/request-form-models/image01.png">
6565
</div>
66+
67+
## Restrict Extra Form Fields
68+
69+
In some special use cases (probably not very common), you might want to **restrict** the form fields to only those declared in the Pydantic model. And **forbid** any **extra** fields.
70+
71+
/// note
72+
73+
This is supported since FastAPI version `0.114.0`. 🤓
74+
75+
///
76+
77+
You can use Pydantic's model configuration to `forbid` any `extra` fields:
78+
79+
//// tab | Python 3.9+
80+
81+
```Python hl_lines="12"
82+
{!> ../../../docs_src/request_form_models/tutorial002_an_py39.py!}
83+
```
84+
85+
////
86+
87+
//// tab | Python 3.8+
88+
89+
```Python hl_lines="11"
90+
{!> ../../../docs_src/request_form_models/tutorial002_an.py!}
91+
```
92+
93+
////
94+
95+
//// tab | Python 3.8+ non-Annotated
96+
97+
/// tip
98+
99+
Prefer to use the `Annotated` version if possible.
100+
101+
///
102+
103+
```Python hl_lines="10"
104+
{!> ../../../docs_src/request_form_models/tutorial002.py!}
105+
```
106+
107+
////
108+
109+
If a client tries to send some extra data, they will receive an **error** response.
110+
111+
For example, if the client tries to send the form fields:
112+
113+
* `username`: `Rick`
114+
* `password`: `Portal Gun`
115+
* `extra`: `Mr. Poopybutthole`
116+
117+
They will receive an error response telling them that the field `extra` is not allowed:
118+
119+
```json
120+
{
121+
"detail": [
122+
{
123+
"type": "extra_forbidden",
124+
"loc": ["body", "extra"],
125+
"msg": "Extra inputs are not permitted",
126+
"input": "Mr. Poopybutthole"
127+
}
128+
]
129+
}
130+
```
131+
132+
## Summary
133+
134+
You can use Pydantic models to declare form fields in FastAPI. 😎
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from fastapi import FastAPI, Form
2+
from pydantic import BaseModel
3+
4+
app = FastAPI()
5+
6+
7+
class FormData(BaseModel):
8+
username: str
9+
password: str
10+
model_config = {"extra": "forbid"}
11+
12+
13+
@app.post("/login/")
14+
async def login(data: FormData = Form()):
15+
return data
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from fastapi import FastAPI, Form
2+
from pydantic import BaseModel
3+
from typing_extensions import Annotated
4+
5+
app = FastAPI()
6+
7+
8+
class FormData(BaseModel):
9+
username: str
10+
password: str
11+
model_config = {"extra": "forbid"}
12+
13+
14+
@app.post("/login/")
15+
async def login(data: Annotated[FormData, Form()]):
16+
return data
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Annotated
2+
3+
from fastapi import FastAPI, Form
4+
from pydantic import BaseModel
5+
6+
app = FastAPI()
7+
8+
9+
class FormData(BaseModel):
10+
username: str
11+
password: str
12+
model_config = {"extra": "forbid"}
13+
14+
15+
@app.post("/login/")
16+
async def login(data: Annotated[FormData, Form()]):
17+
return data
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from fastapi import FastAPI, Form
2+
from pydantic import BaseModel
3+
4+
app = FastAPI()
5+
6+
7+
class FormData(BaseModel):
8+
username: str
9+
password: str
10+
11+
class Config:
12+
extra = "forbid"
13+
14+
15+
@app.post("/login/")
16+
async def login(data: FormData = Form()):
17+
return data
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from fastapi import FastAPI, Form
2+
from pydantic import BaseModel
3+
from typing_extensions import Annotated
4+
5+
app = FastAPI()
6+
7+
8+
class FormData(BaseModel):
9+
username: str
10+
password: str
11+
12+
class Config:
13+
extra = "forbid"
14+
15+
16+
@app.post("/login/")
17+
async def login(data: Annotated[FormData, Form()]):
18+
return data
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import Annotated
2+
3+
from fastapi import FastAPI, Form
4+
from pydantic import BaseModel
5+
6+
app = FastAPI()
7+
8+
9+
class FormData(BaseModel):
10+
username: str
11+
password: str
12+
13+
class Config:
14+
extra = "forbid"
15+
16+
17+
@app.post("/login/")
18+
async def login(data: Annotated[FormData, Form()]):
19+
return data

fastapi/dependencies/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,9 @@ async def process_fn(
789789
value = serialize_sequence_value(field=field, value=results)
790790
if value is not None:
791791
values[field.name] = value
792+
for key, value in received_body.items():
793+
if key not in values:
794+
values[key] = value
792795
return values
793796

794797

0 commit comments

Comments
 (0)