From eec2fe903382c48a1dbbd7582b11e04d8c781cf1 Mon Sep 17 00:00:00 2001 From: Alberto Zambrano Date: Wed, 21 Jan 2026 13:41:52 +0100 Subject: [PATCH 1/2] docs: clarify required but nullable request body fields --- docs/en/docs/tutorial/body.md | 30 +++++++++++++++++++ docs_src/body/tutorial005_py310.py | 11 +++++++ docs_src/body/tutorial005_py39.py | 12 ++++++++ .../test_body_required_nullable/__init__.py | 0 .../test_tutorial001.py | 28 +++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 docs_src/body/tutorial005_py310.py create mode 100644 docs_src/body/tutorial005_py39.py create mode 100644 tests/test_tutorial/test_body_required_nullable/__init__.py create mode 100644 tests/test_tutorial/test_body_required_nullable/test_tutorial001.py diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 2d0dfcbb59..c03fcb4a5b 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -55,6 +55,36 @@ For example, this model above declares a JSON "`object`" (or Python `dict`) like } ``` +### 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 `Optional[T]` **without** providing a default value. + +For example: + +{* ../../docs_src/body/tutorial005_py39.py hl[6] *} + +And for Python 3.10+: + +{* ../../docs_src/body/tutorial005_py310.py hl[6] *} + +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: Optional[str] = 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 0000000000..2476a23f3e --- /dev/null +++ b/docs_src/body/tutorial005_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + +class Item(BaseModel): + description: str | None + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/docs_src/body/tutorial005_py39.py b/docs_src/body/tutorial005_py39.py new file mode 100644 index 0000000000..4529234d0f --- /dev/null +++ b/docs_src/body/tutorial005_py39.py @@ -0,0 +1,12 @@ +from typing import Optional +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + +class Item(BaseModel): + description: Optional[str] + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/tests/test_tutorial/test_body_required_nullable/__init__.py b/tests/test_tutorial/test_body_required_nullable/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_body_required_nullable/test_tutorial001.py b/tests/test_tutorial/test_body_required_nullable/test_tutorial001.py new file mode 100644 index 0000000000..2c60356c3f --- /dev/null +++ b/tests/test_tutorial/test_body_required_nullable/test_tutorial001.py @@ -0,0 +1,28 @@ +import importlib +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_py310", marks=needs_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": None}) + assert response.status_code == 200 + assert response.json() == {"description": None} + + +def test_required_field_missing(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 From ee496848c893d84351547a60fc9187b6ddbc6bc8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:23:43 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/body.md | 2 +- docs_src/body/tutorial005_py310.py | 2 ++ docs_src/body/tutorial005_py39.py | 3 +++ .../test_body_required_nullable/test_tutorial001.py | 2 ++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index c03fcb4a5b..91f4b8ae70 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -55,7 +55,7 @@ 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` { #required-fields-that-can-be-none } In Python type hints, a parameter can be **required** and still allow the value `None`. diff --git a/docs_src/body/tutorial005_py310.py b/docs_src/body/tutorial005_py310.py index 2476a23f3e..7a9c9c9d92 100644 --- a/docs_src/body/tutorial005_py310.py +++ b/docs_src/body/tutorial005_py310.py @@ -3,9 +3,11 @@ from pydantic import BaseModel app = FastAPI() + class Item(BaseModel): description: str | None + @app.post("/items/") async def create_item(item: Item): return item diff --git a/docs_src/body/tutorial005_py39.py b/docs_src/body/tutorial005_py39.py index 4529234d0f..32420e2820 100644 --- a/docs_src/body/tutorial005_py39.py +++ b/docs_src/body/tutorial005_py39.py @@ -1,12 +1,15 @@ from typing import Optional + from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() + class Item(BaseModel): description: Optional[str] + @app.post("/items/") async def create_item(item: Item): return item diff --git a/tests/test_tutorial/test_body_required_nullable/test_tutorial001.py b/tests/test_tutorial/test_body_required_nullable/test_tutorial001.py index 2c60356c3f..ed12688663 100644 --- a/tests/test_tutorial/test_body_required_nullable/test_tutorial001.py +++ b/tests/test_tutorial/test_body_required_nullable/test_tutorial001.py @@ -1,9 +1,11 @@ import importlib + import pytest from fastapi.testclient import TestClient from ...utils import needs_py310 + @pytest.fixture( name="client", params=[