diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 2d0dfcbb59..91f4b8ae70 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` { #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..7a9c9c9d92 --- /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 | 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..32420e2820 --- /dev/null +++ b/docs_src/body/tutorial005_py39.py @@ -0,0 +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/__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..ed12688663 --- /dev/null +++ b/tests/test_tutorial/test_body_required_nullable/test_tutorial001.py @@ -0,0 +1,30 @@ +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