forked from fastapi/fastapi
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Add support for Pydantic models in
Form
parameters (fastapi#12129)
Revert "⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` pa…" This reverts commit 8e6cf9e.
- Loading branch information
Showing
13 changed files
with
994 additions
and
3 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Form Models | ||
|
||
You can use Pydantic models to declare form fields in FastAPI. | ||
|
||
/// info | ||
|
||
To use forms, first install <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>. | ||
|
||
Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: | ||
|
||
```console | ||
$ pip install python-multipart | ||
``` | ||
|
||
/// | ||
|
||
/// note | ||
|
||
This is supported since FastAPI version `0.113.0`. 🤓 | ||
|
||
/// | ||
|
||
## Pydantic Models for Forms | ||
|
||
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`: | ||
|
||
//// tab | Python 3.9+ | ||
|
||
```Python hl_lines="9-11 15" | ||
{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!} | ||
``` | ||
|
||
//// | ||
|
||
//// tab | Python 3.8+ | ||
|
||
```Python hl_lines="8-10 14" | ||
{!> ../../../docs_src/request_form_models/tutorial001_an.py!} | ||
``` | ||
|
||
//// | ||
|
||
//// tab | Python 3.8+ non-Annotated | ||
|
||
/// tip | ||
|
||
Prefer to use the `Annotated` version if possible. | ||
|
||
/// | ||
|
||
```Python hl_lines="7-9 13" | ||
{!> ../../../docs_src/request_form_models/tutorial001.py!} | ||
``` | ||
|
||
//// | ||
|
||
FastAPI will extract the data for each field from the form data in the request and give you the Pydantic model you defined. | ||
|
||
## Check the Docs | ||
|
||
You can verify it in the docs UI at `/docs`: | ||
|
||
<div class="screenshot"> | ||
<img src="/img/tutorial/request-form-models/image01.png"> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from fastapi import FastAPI, Form | ||
from pydantic import BaseModel | ||
|
||
app = FastAPI() | ||
|
||
|
||
class FormData(BaseModel): | ||
username: str | ||
password: str | ||
|
||
|
||
@app.post("/login/") | ||
async def login(data: FormData = Form()): | ||
return data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from fastapi import FastAPI, Form | ||
from pydantic import BaseModel | ||
from typing_extensions import Annotated | ||
|
||
app = FastAPI() | ||
|
||
|
||
class FormData(BaseModel): | ||
username: str | ||
password: str | ||
|
||
|
||
@app.post("/login/") | ||
async def login(data: Annotated[FormData, Form()]): | ||
return data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from typing import Annotated | ||
|
||
from fastapi import FastAPI, Form | ||
from pydantic import BaseModel | ||
|
||
app = FastAPI() | ||
|
||
|
||
class FormData(BaseModel): | ||
username: str | ||
password: str | ||
|
||
|
||
@app.post("/login/") | ||
async def login(data: Annotated[FormData, Form()]): | ||
return data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import subprocess | ||
import time | ||
|
||
import httpx | ||
from playwright.sync_api import Playwright, sync_playwright | ||
|
||
|
||
# Run playwright codegen to generate the code below, copy paste the sections in run() | ||
def run(playwright: Playwright) -> None: | ||
browser = playwright.chromium.launch(headless=False) | ||
context = browser.new_context() | ||
page = context.new_page() | ||
page.goto("http://localhost:8000/docs") | ||
page.get_by_role("button", name="POST /login/ Login").click() | ||
page.get_by_role("button", name="Try it out").click() | ||
page.screenshot(path="docs/en/docs/img/tutorial/request-form-models/image01.png") | ||
|
||
# --------------------- | ||
context.close() | ||
browser.close() | ||
|
||
|
||
process = subprocess.Popen( | ||
["fastapi", "run", "docs_src/request_form_models/tutorial001.py"] | ||
) | ||
try: | ||
for _ in range(3): | ||
try: | ||
response = httpx.get("http://localhost:8000/docs") | ||
except httpx.ConnectError: | ||
time.sleep(1) | ||
break | ||
with sync_playwright() as playwright: | ||
run(playwright) | ||
finally: | ||
process.terminate() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
from typing import List, Optional | ||
|
||
from dirty_equals import IsDict | ||
from fastapi import FastAPI, Form | ||
from fastapi.testclient import TestClient | ||
from pydantic import BaseModel | ||
from typing_extensions import Annotated | ||
|
||
app = FastAPI() | ||
|
||
|
||
class FormModel(BaseModel): | ||
username: str | ||
lastname: str | ||
age: Optional[int] = None | ||
tags: List[str] = ["foo", "bar"] | ||
|
||
|
||
@app.post("/form/") | ||
def post_form(user: Annotated[FormModel, Form()]): | ||
return user | ||
|
||
|
||
client = TestClient(app) | ||
|
||
|
||
def test_send_all_data(): | ||
response = client.post( | ||
"/form/", | ||
data={ | ||
"username": "Rick", | ||
"lastname": "Sanchez", | ||
"age": "70", | ||
"tags": ["plumbus", "citadel"], | ||
}, | ||
) | ||
assert response.status_code == 200, response.text | ||
assert response.json() == { | ||
"username": "Rick", | ||
"lastname": "Sanchez", | ||
"age": 70, | ||
"tags": ["plumbus", "citadel"], | ||
} | ||
|
||
|
||
def test_defaults(): | ||
response = client.post("/form/", data={"username": "Rick", "lastname": "Sanchez"}) | ||
assert response.status_code == 200, response.text | ||
assert response.json() == { | ||
"username": "Rick", | ||
"lastname": "Sanchez", | ||
"age": None, | ||
"tags": ["foo", "bar"], | ||
} | ||
|
||
|
||
def test_invalid_data(): | ||
response = client.post( | ||
"/form/", | ||
data={ | ||
"username": "Rick", | ||
"lastname": "Sanchez", | ||
"age": "seventy", | ||
"tags": ["plumbus", "citadel"], | ||
}, | ||
) | ||
assert response.status_code == 422, response.text | ||
assert response.json() == IsDict( | ||
{ | ||
"detail": [ | ||
{ | ||
"type": "int_parsing", | ||
"loc": ["body", "age"], | ||
"msg": "Input should be a valid integer, unable to parse string as an integer", | ||
"input": "seventy", | ||
} | ||
] | ||
} | ||
) | IsDict( | ||
# TODO: remove when deprecating Pydantic v1 | ||
{ | ||
"detail": [ | ||
{ | ||
"loc": ["body", "age"], | ||
"msg": "value is not a valid integer", | ||
"type": "type_error.integer", | ||
} | ||
] | ||
} | ||
) | ||
|
||
|
||
def test_no_data(): | ||
response = client.post("/form/") | ||
assert response.status_code == 422, response.text | ||
assert response.json() == IsDict( | ||
{ | ||
"detail": [ | ||
{ | ||
"type": "missing", | ||
"loc": ["body", "username"], | ||
"msg": "Field required", | ||
"input": {"tags": ["foo", "bar"]}, | ||
}, | ||
{ | ||
"type": "missing", | ||
"loc": ["body", "lastname"], | ||
"msg": "Field required", | ||
"input": {"tags": ["foo", "bar"]}, | ||
}, | ||
] | ||
} | ||
) | IsDict( | ||
# TODO: remove when deprecating Pydantic v1 | ||
{ | ||
"detail": [ | ||
{ | ||
"loc": ["body", "username"], | ||
"msg": "field required", | ||
"type": "value_error.missing", | ||
}, | ||
{ | ||
"loc": ["body", "lastname"], | ||
"msg": "field required", | ||
"type": "value_error.missing", | ||
}, | ||
] | ||
} | ||
) |
Empty file.
Oops, something went wrong.