diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index fb4471836..72ec94fe6 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -55,6 +55,32 @@ For example, this model above declares a JSON "`object`" (or Python `dict`) like } ``` +### Required fields that can be `None` { #required-fields-that-can-be-none } + +In Python type hints, a parameter can be **required** and still allow the value `None`. + +This means that the field must be present in the request body, but its value can be `null` +(`None` in Python). + +This typically happens when you use something like `str | None` (or `Optional[str]`) **without** providing a default value. + +For example: + +{* ../../docs_src/body/tutorial005_py310.py hl[8] *} + +In this example: + +* The `description` field is **required** (the client must include it in the JSON body). +* Its value can still be `null` (`None` in Python). +* This is different from a truly optional field, which would have a default value: + +```python +class Item(BaseModel): + description: str | None = None +``` + +Here, `description` can be omitted entirely in the request body. + ## Declare it as a parameter { #declare-it-as-a-parameter } To add it to your *path operation*, declare it the same way you declared path and query parameters: diff --git a/docs_src/body/tutorial005_py310.py b/docs_src/body/tutorial005_py310.py new file mode 100644 index 000000000..cd487a75a --- /dev/null +++ b/docs_src/body/tutorial005_py310.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + description: str + + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/tests/test_tutorial/test_body/test_tutorial005.py b/tests/test_tutorial/test_body/test_tutorial005.py new file mode 100644 index 000000000..4e0053af1 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial005.py @@ -0,0 +1,27 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py310"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.body.{request.param}") + client = TestClient(mod.app) + return client + + +def test_required_nullable_field(client: TestClient): + response = client.post("/items/", json={"description": "Some description"}) + assert response.status_code == 200 + assert response.json() == {"description": "Some description"} + + +def test_required_field_missing(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422