From 0f3e65b00712a59d763cf9c7715cde353bb94b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 16:40:48 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Pydantic=20mo?= =?UTF-8?q?dels=20in=20`Form`=20parameters=20(#12127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/request-form-models/image01.png | Bin 0 -> 44487 bytes docs/en/docs/tutorial/request-form-models.md | 65 +++++ docs/en/mkdocs.yml | 1 + docs_src/request_form_models/tutorial001.py | 14 + .../request_form_models/tutorial001_an.py | 15 ++ .../tutorial001_an_py39.py | 16 ++ fastapi/dependencies/utils.py | 17 +- .../playwright/request_form_models/image01.py | 36 +++ tests/test_forms_single_model.py | 129 ++++++++++ .../test_request_form_models/__init__.py | 0 .../test_tutorial001.py | 232 +++++++++++++++++ .../test_tutorial001_an.py | 232 +++++++++++++++++ .../test_tutorial001_an_py39.py | 240 ++++++++++++++++++ 13 files changed, 994 insertions(+), 3 deletions(-) create mode 100644 docs/en/docs/img/tutorial/request-form-models/image01.png create mode 100644 docs/en/docs/tutorial/request-form-models.md create mode 100644 docs_src/request_form_models/tutorial001.py create mode 100644 docs_src/request_form_models/tutorial001_an.py create mode 100644 docs_src/request_form_models/tutorial001_an_py39.py create mode 100644 scripts/playwright/request_form_models/image01.py create mode 100644 tests/test_forms_single_model.py create mode 100644 tests/test_tutorial/test_request_form_models/__init__.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py diff --git a/docs/en/docs/img/tutorial/request-form-models/image01.png b/docs/en/docs/img/tutorial/request-form-models/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe32c03d589e76abec5e9c6b71374ebd4e8cd2c GIT binary patch literal 44487 zcmeFZcT|(j_b-b2Dz8{DDj*>68l^Ys(k&DL0RgF@_uhL8ib&{6RjSe3GerJ&n@emd;U6qoOQFZ);yV+XJ+p`lX>?3?7g3`_bT#tDCj82$jI&} zyp>TWBfCz#ygKyvRbugyN%0D?xZ(r=sS4^yATZ0)ghX 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 CMaG{1 literal 0 HcmV?d00001 diff --git a/docs/en/docs/tutorial/request-form-models.md b/docs/en/docs/tutorial/request-form-models.md new file mode 100644 index 000000000..8bb1ffb1f --- /dev/null +++ b/docs/en/docs/tutorial/request-form-models.md @@ -0,0 +1,65 @@ +# Form Models + +You can use Pydantic models to declare form fields in FastAPI. + +/// info + +To use forms, first install `python-multipart`. + +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`: + +
+ +
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 528c80b8e..7c810c2d7 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -129,6 +129,7 @@ nav: - tutorial/extra-models.md - tutorial/response-status-code.md - tutorial/request-forms.md + - tutorial/request-form-models.md - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md diff --git a/docs_src/request_form_models/tutorial001.py b/docs_src/request_form_models/tutorial001.py new file mode 100644 index 000000000..98feff0b9 --- /dev/null +++ b/docs_src/request_form_models/tutorial001.py @@ -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 diff --git a/docs_src/request_form_models/tutorial001_an.py b/docs_src/request_form_models/tutorial001_an.py new file mode 100644 index 000000000..30483d445 --- /dev/null +++ b/docs_src/request_form_models/tutorial001_an.py @@ -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 diff --git a/docs_src/request_form_models/tutorial001_an_py39.py b/docs_src/request_form_models/tutorial001_an_py39.py new file mode 100644 index 000000000..7cc81aae9 --- /dev/null +++ b/docs_src/request_form_models/tutorial001_an_py39.py @@ -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 diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 7ac18d941..98ce17b55 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -33,6 +33,7 @@ from fastapi._compat import ( field_annotation_is_scalar, get_annotation_from_field_info, get_missing_field_error, + get_model_fields, is_bytes_field, is_bytes_sequence_field, is_scalar_field, @@ -56,6 +57,7 @@ from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import create_model_field, get_path_param_names +from pydantic import BaseModel from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool @@ -743,7 +745,9 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: return True # If it's a Form (or File) field, it has to be a BaseModel to be top level # otherwise it has to be embedded, so that the key value pair can be extracted - if isinstance(first_field.field_info, params.Form): + if isinstance(first_field.field_info, params.Form) and not lenient_issubclass( + first_field.type_, BaseModel + ): return True return False @@ -783,7 +787,8 @@ async def _extract_form_body( for sub_value in value: tg.start_soon(process_fn, sub_value.read) value = serialize_sequence_value(field=field, value=results) - values[field.name] = value + if value is not None: + values[field.name] = value return values @@ -798,8 +803,14 @@ async def request_body_to_args( single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields first_field = body_fields[0] body_to_process = received_body + + fields_to_extract: List[ModelField] = body_fields + + if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel): + fields_to_extract = get_model_fields(first_field.type_) + if isinstance(received_body, FormData): - body_to_process = await _extract_form_body(body_fields, received_body) + body_to_process = await _extract_form_body(fields_to_extract, received_body) if single_not_embedded_field: loc: Tuple[str, ...] = ("body",) diff --git a/scripts/playwright/request_form_models/image01.py b/scripts/playwright/request_form_models/image01.py new file mode 100644 index 000000000..15bd3858c --- /dev/null +++ b/scripts/playwright/request_form_models/image01.py @@ -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() diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py new file mode 100644 index 000000000..7ed3ba3a2 --- /dev/null +++ b/tests/test_forms_single_model.py @@ -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", + }, + ] + } + ) diff --git a/tests/test_tutorial/test_request_form_models/__init__.py b/tests/test_tutorial/test_request_form_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py new file mode 100644 index 000000000..46c130ee8 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -0,0 +1,232 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001 import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py new file mode 100644 index 000000000..4e14d89c8 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py @@ -0,0 +1,232 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py new file mode 100644 index 000000000..2e6426aa7 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py @@ -0,0 +1,240 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from tests.utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + }