From eec2fe903382c48a1dbbd7582b11e04d8c781cf1 Mon Sep 17 00:00:00 2001 From: Alberto Zambrano Date: Wed, 21 Jan 2026 13:41:52 +0100 Subject: [PATCH] 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