From c841c429293a606e805b0a26959c13732c398392 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 10:32:46 +0530 Subject: [PATCH 01/16] docs: clarify misleading 'Required, can be None' section in query param validation --- .../tutorial/query-params-str-validations.md | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index adf08a924..332636080 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -260,11 +260,34 @@ So, when you need to declare a value as required while using `Query`, you can si ### Required, can be `None` { #required-can-be-none } -You can declare that a parameter can accept `None`, but that it's still required. This would force clients to send a value, even if the value is `None`. +You might want to declare that a parameter can accept `None`, but is still required. -To do that, you can declare that `None` is a valid type but simply do not declare a default value: +However, because of how **HTTP query parameters** work, clients cannot actually send a real `None` (or `null`) value — query parameters are always sent as **strings**. +That means you cannot truly have a *required* parameter that also allows a real `None` value. -{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} +For example, you might try: + +```Python +q: Annotated[str | None, Query(min_length=3)] = ... +``` + +But this will still expect a string value, and if the client omits q or tries to send q=None, FastAPI will raise a validation error. +In other words, None is not an acceptable runtime value for query parameters — only strings are. + +If you want to accept special values (like "None" or an empty string) and interpret them as None in your application, you can handle them manually in your function: + +```Python +from fastapi import FastAPI, Query +from typing import Annotated + +app = FastAPI() + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query()] = ...): + if q in ("None", "", "null"): + q = None + return {"q": q} +``` ## Query parameter list / multiple values { #query-parameter-list-multiple-values } From b1c309a666dcecad92b8169537aaaf93cfdabb24 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 14:55:38 +0530 Subject: [PATCH 02/16] docs: fix inline code formatting and add tested example for Required, can be None section --- .../tutorial/query-params-str-validations.md | 19 ++++--------------- .../tutorial006d_an_py310.py | 10 ++++++++++ .../test_tutorial006d_an_py310.py | 9 +++++++++ 3 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 docs_src/query_params_str_validations/tutorial006d_an_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 332636080..d709fe561 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -271,23 +271,12 @@ For example, you might try: q: Annotated[str | None, Query(min_length=3)] = ... ``` -But this will still expect a string value, and if the client omits q or tries to send q=None, FastAPI will raise a validation error. -In other words, None is not an acceptable runtime value for query parameters — only strings are. +But this will still expect a **string** value, and if the client omits `q` or tries to send `q=None`, FastAPI will raise a validation error. +In other words, `None` is not an acceptable runtime value for query parameters — only strings are. -If you want to accept special values (like "None" or an empty string) and interpret them as None in your application, you can handle them manually in your function: +If you want to accept special values (like `"None"` or an empty string) and interpret them as `None` in your application, you can handle them manually in your function: -```Python -from fastapi import FastAPI, Query -from typing import Annotated - -app = FastAPI() - -@app.get("/items/") -async def read_items(q: Annotated[str | None, Query()] = ...): - if q in ("None", "", "null"): - q = None - return {"q": q} -``` +{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9] *} ## Query parameter list / multiple values { #query-parameter-list-multiple-values } diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py new file mode 100644 index 000000000..0962fbce7 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an_py310.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI, Query +from typing import Annotated + +app = FastAPI() + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query()] = ...): + if q in ("None", "", "null"): + q = None + return {"q": q} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py new file mode 100644 index 000000000..57fccec94 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py @@ -0,0 +1,9 @@ +from fastapi.testclient import TestClient +from docs_src.query_params_str_validations.tutorial006d_an_py310 import app + +client = TestClient(app) + +def test_read_items(): + response = client.get("/items/", params={"q": "None"}) + assert response.status_code == 200 + assert response.json() == {"q": None} From dc7285d31a315e8ca0bbb8c3fdb02293af3be1b2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:25:49 +0000 Subject: [PATCH 03/16] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/query-params-str-validations.md | 2 +- .../query_params_str_validations/tutorial006d_an_py310.py | 4 +++- .../test_tutorial006d_an_py310.py | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index d709fe561..da89cec17 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -271,7 +271,7 @@ For example, you might try: q: Annotated[str | None, Query(min_length=3)] = ... ``` -But this will still expect a **string** value, and if the client omits `q` or tries to send `q=None`, FastAPI will raise a validation error. +But this will still expect a **string** value, and if the client omits `q` or tries to send `q=None`, FastAPI will raise a validation error. In other words, `None` is not an acceptable runtime value for query parameters — only strings are. If you want to accept special values (like `"None"` or an empty string) and interpret them as `None` in your application, you can handle them manually in your function: diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py index 0962fbce7..025d314ea 100644 --- a/docs_src/query_params_str_validations/tutorial006d_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006d_an_py310.py @@ -1,8 +1,10 @@ -from fastapi import FastAPI, Query from typing import Annotated +from fastapi import FastAPI, Query + app = FastAPI() + @app.get("/items/") async def read_items(q: Annotated[str | None, Query()] = ...): if q in ("None", "", "null"): diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py index 57fccec94..36ae95f65 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py @@ -1,8 +1,10 @@ from fastapi.testclient import TestClient + from docs_src.query_params_str_validations.tutorial006d_an_py310 import app client = TestClient(app) + def test_read_items(): response = client.get("/items/", params={"q": "None"}) assert response.status_code == 200 From 3259949c4ed62e3091806633614a1610879a8290 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 15:05:37 +0530 Subject: [PATCH 04/16] docs: finalize Required, can be None example with test verification --- .../query_params_str_validations/tutorial006d_an_py310.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py index 025d314ea..2fd8923bf 100644 --- a/docs_src/query_params_str_validations/tutorial006d_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006d_an_py310.py @@ -1,12 +1,11 @@ -from typing import Annotated - from fastapi import FastAPI, Query +from typing import Annotated, Optional app = FastAPI() @app.get("/items/") -async def read_items(q: Annotated[str | None, Query()] = ...): +async def read_items(q: Annotated[Optional[str], Query()] = ...): if q in ("None", "", "null"): q = None return {"q": q} From bb64c976b9ff46c2990da77761a7f6fee90c0fd2 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 15:45:23 +0530 Subject: [PATCH 05/16] style: clean formatting with Ruff + Black --- .../tutorial/query-params-str-validations.md | 4 ++-- .../tutorial006d_an_py310.py | 16 ++++++++++++---- .../test_tutorial006d_an_py310.py | 16 +++++++++++++--- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index da89cec17..f473958de 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -271,12 +271,12 @@ For example, you might try: q: Annotated[str | None, Query(min_length=3)] = ... ``` -But this will still expect a **string** value, and if the client omits `q` or tries to send `q=None`, FastAPI will raise a validation error. +But this will still expect a **string** value, and if the client omits `q` or tries to send `q=None`, FastAPI will raise a **request validation error**. In other words, `None` is not an acceptable runtime value for query parameters — only strings are. If you want to accept special values (like `"None"` or an empty string) and interpret them as `None` in your application, you can handle them manually in your function: -{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9,11] *} ## Query parameter list / multiple values { #query-parameter-list-multiple-values } diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py index 2fd8923bf..9a9d082bf 100644 --- a/docs_src/query_params_str_validations/tutorial006d_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006d_an_py310.py @@ -1,11 +1,19 @@ +from typing import Annotated, Optional, Union + from fastapi import FastAPI, Query -from typing import Annotated, Optional +from pydantic import BeforeValidator app = FastAPI() +def nullable_str(val: str) -> Union[str, None]: + if val in ("None", "", "null"): + return None + return val + + @app.get("/items/") -async def read_items(q: Annotated[Optional[str], Query()] = ...): - if q in ("None", "", "null"): - q = None +async def read_items( + q: Annotated[Optional[str], Query(min_length=3), BeforeValidator(nullable_str)] +): return {"q": q} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py index 36ae95f65..af7e9dd09 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py @@ -1,3 +1,4 @@ +import pytest from fastapi.testclient import TestClient from docs_src.query_params_str_validations.tutorial006d_an_py310 import app @@ -5,7 +6,16 @@ from docs_src.query_params_str_validations.tutorial006d_an_py310 import app client = TestClient(app) -def test_read_items(): - response = client.get("/items/", params={"q": "None"}) +@pytest.mark.parametrize( + "q_value,expected", + [ + ("None", None), + ("", None), + ("null", None), + ("hello", "hello"), + ], +) +def test_read_items(q_value, expected): + response = client.get("/items/", params={"q": q_value}) assert response.status_code == 200 - assert response.json() == {"q": None} + assert response.json() == {"q": expected} From 58f9c57a47a0d3a050c1674ee97ce9c5a24e68e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:16:31 +0000 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/query_params_str_validations/tutorial006d_an_py310.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py index 9a9d082bf..94af45722 100644 --- a/docs_src/query_params_str_validations/tutorial006d_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006d_an_py310.py @@ -14,6 +14,6 @@ def nullable_str(val: str) -> Union[str, None]: @app.get("/items/") async def read_items( - q: Annotated[Optional[str], Query(min_length=3), BeforeValidator(nullable_str)] + q: Annotated[Optional[str], Query(min_length=3), BeforeValidator(nullable_str)], ): return {"q": q} From d9fa14d46d07a13ca066b00cadb8ee56e69a9477 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 16:08:08 +0530 Subject: [PATCH 07/16] fix: import Annotated from typing_extensions for Python 3.8 compatibility --- docs_src/query_params_str_validations/tutorial006d_an_py310.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py index 94af45722..6e58cc8f0 100644 --- a/docs_src/query_params_str_validations/tutorial006d_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006d_an_py310.py @@ -1,4 +1,5 @@ -from typing import Annotated, Optional, Union +from typing import Optional, Union +from typing_extensions import Annotated from fastapi import FastAPI, Query from pydantic import BeforeValidator From 6d334327d849b3d614b28975cc36ec8ac3c3a29e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:39:51 +0000 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/query_params_str_validations/tutorial006d_an_py310.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py index 6e58cc8f0..344cf38e5 100644 --- a/docs_src/query_params_str_validations/tutorial006d_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006d_an_py310.py @@ -1,8 +1,8 @@ from typing import Optional, Union -from typing_extensions import Annotated from fastapi import FastAPI, Query from pydantic import BeforeValidator +from typing_extensions import Annotated app = FastAPI() From 048d7ad3bb9439c665607d40da1ad63803bda9a6 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 16:21:20 +0530 Subject: [PATCH 09/16] test: finalize client fixture and absolute import for needs_pydanticv2 --- .../test_tutorial006d_an_py310.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py index af7e9dd09..113ab2f93 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py @@ -1,11 +1,14 @@ import pytest from fastapi.testclient import TestClient +from tests.utils import needs_pydanticv2 -from docs_src.query_params_str_validations.tutorial006d_an_py310 import app - -client = TestClient(app) +@pytest.fixture +def client(): + from docs_src.query_params_str_validations.tutorial006d_an_py310 import app + yield TestClient(app) +@needs_pydanticv2 @pytest.mark.parametrize( "q_value,expected", [ @@ -15,7 +18,7 @@ client = TestClient(app) ("hello", "hello"), ], ) -def test_read_items(q_value, expected): +def test_read_items(q_value, expected, client: TestClient): response = client.get("/items/", params={"q": q_value}) assert response.status_code == 200 assert response.json() == {"q": expected} From 41179df56c6a7b87f7b606b49b381d4db9e141d2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:52:00 +0000 Subject: [PATCH 10/16] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_tutorial006d_an_py310.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py index 113ab2f93..86c13cd2e 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py @@ -1,10 +1,13 @@ import pytest from fastapi.testclient import TestClient + from tests.utils import needs_pydanticv2 + @pytest.fixture def client(): from docs_src.query_params_str_validations.tutorial006d_an_py310 import app + yield TestClient(app) From 16d4d14578035377d518e91ee2165d7e609f8e2a Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Tue, 11 Nov 2025 16:31:32 +0530 Subject: [PATCH 11/16] docs: clarify BeforeValidator is available only in Pydantic v2 --- docs/en/docs/tutorial/query-params-str-validations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index f473958de..f64b980c4 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -278,6 +278,8 @@ If you want to accept special values (like `"None"` or an empty string) and inte {* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9,11] *} +> **Note**: This example uses `BeforeValidator`, which is only available in **Pydantic v2**. + ## Query parameter list / multiple values { #query-parameter-list-multiple-values } When you define a query parameter explicitly with `Query` you can also declare it to receive a list of values, or said in another way, to receive multiple values. From 637510f429d8625d6e3e0ad7500491ff35935084 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:16:34 +0100 Subject: [PATCH 12/16] Apply suggestions from code review --- docs/en/docs/tutorial/query-params-str-validations.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index f64b980c4..952a63796 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -276,9 +276,13 @@ In other words, `None` is not an acceptable runtime value for query parameters If you want to accept special values (like `"None"` or an empty string) and interpret them as `None` in your application, you can handle them manually in your function: -{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9,11] *} +{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9:13 18] *} -> **Note**: This example uses `BeforeValidator`, which is only available in **Pydantic v2**. +/// note + +This example uses `BeforeValidator`, which is only available in **Pydantic v2**. + +/// ## Query parameter list / multiple values { #query-parameter-list-multiple-values } From c8eabfa411c4a4920412c165e1ca70dd3cafa234 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:20:46 +0100 Subject: [PATCH 13/16] Update docs/en/docs/tutorial/query-params-str-validations.md --- docs/en/docs/tutorial/query-params-str-validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 952a63796..5dfac9537 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -276,7 +276,7 @@ In other words, `None` is not an acceptable runtime value for query parameters If you want to accept special values (like `"None"` or an empty string) and interpret them as `None` in your application, you can handle them manually in your function: -{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9:13 18] *} +{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9:13,18] *} /// note From f223d0ce69210d824891dd8b0da8793eb1d2a0f5 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:25:15 +0100 Subject: [PATCH 14/16] Update highlights --- docs/en/docs/tutorial/query-params-str-validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 5dfac9537..0f87412f9 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -276,7 +276,7 @@ In other words, `None` is not an acceptable runtime value for query parameters If you want to accept special values (like `"None"` or an empty string) and interpret them as `None` in your application, you can handle them manually in your function: -{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[9:13,18] *} +{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[10:13,18] *} /// note From 48a876d54afe486f409a1deb4868eaa4a3f224f4 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Fri, 28 Nov 2025 10:51:41 +0100 Subject: [PATCH 15/16] Rename code example file and add variants --- .../tutorial/query-params-str-validations.md | 2 +- .../tutorial006c.py | 13 ------------ .../tutorial006c_an.py | 18 +++++++++++------ .../tutorial006c_an_py310.py | 16 ++++++++++----- .../tutorial006c_an_py39.py | 18 +++++++++++------ .../tutorial006c_py310.py | 11 ---------- .../tutorial006d_an_py310.py | 20 ------------------- ...l006d_an_py310.py => test_tutorial006c.py} | 20 +++++++++++++++---- 8 files changed, 52 insertions(+), 66 deletions(-) delete mode 100644 docs_src/query_params_str_validations/tutorial006c.py delete mode 100644 docs_src/query_params_str_validations/tutorial006c_py310.py delete mode 100644 docs_src/query_params_str_validations/tutorial006d_an_py310.py rename tests/test_tutorial/test_query_params_str_validations/{test_tutorial006d_an_py310.py => test_tutorial006c.py} (51%) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 0f87412f9..81633e4b6 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -276,7 +276,7 @@ In other words, `None` is not an acceptable runtime value for query parameters If you want to accept special values (like `"None"` or an empty string) and interpret them as `None` in your application, you can handle them manually in your function: -{* ../../docs_src/query_params_str_validations/tutorial006d_an_py310.py hl[10:13,18] *} +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9:12,17] *} /// note diff --git a/docs_src/query_params_str_validations/tutorial006c.py b/docs_src/query_params_str_validations/tutorial006c.py deleted file mode 100644 index 0a0e820da..000000000 --- a/docs_src/query_params_str_validations/tutorial006c.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Union - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Union[str, None] = Query(min_length=3)): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an.py b/docs_src/query_params_str_validations/tutorial006c_an.py index 55c4f4adc..344cf38e5 100644 --- a/docs_src/query_params_str_validations/tutorial006c_an.py +++ b/docs_src/query_params_str_validations/tutorial006c_an.py @@ -1,14 +1,20 @@ -from typing import Union +from typing import Optional, Union from fastapi import FastAPI, Query +from pydantic import BeforeValidator from typing_extensions import Annotated app = FastAPI() +def nullable_str(val: str) -> Union[str, None]: + if val in ("None", "", "null"): + return None + return val + + @app.get("/items/") -async def read_items(q: Annotated[Union[str, None], Query(min_length=3)]): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results +async def read_items( + q: Annotated[Optional[str], Query(min_length=3), BeforeValidator(nullable_str)], +): + return {"q": q} diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py310.py b/docs_src/query_params_str_validations/tutorial006c_an_py310.py index 2995d9c97..95c63b259 100644 --- a/docs_src/query_params_str_validations/tutorial006c_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006c_an_py310.py @@ -1,13 +1,19 @@ from typing import Annotated from fastapi import FastAPI, Query +from pydantic import BeforeValidator app = FastAPI() +def nullable_str(val: str) -> str | None: + if val in ("None", "", "null"): + return None + return val + + @app.get("/items/") -async def read_items(q: Annotated[str | None, Query(min_length=3)]): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results +async def read_items( + q: Annotated[str | None, Query(min_length=3), BeforeValidator(nullable_str)], +): + return {"q": q} diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py39.py b/docs_src/query_params_str_validations/tutorial006c_an_py39.py index 76a1cd49a..94af45722 100644 --- a/docs_src/query_params_str_validations/tutorial006c_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial006c_an_py39.py @@ -1,13 +1,19 @@ -from typing import Annotated, Union +from typing import Annotated, Optional, Union from fastapi import FastAPI, Query +from pydantic import BeforeValidator app = FastAPI() +def nullable_str(val: str) -> Union[str, None]: + if val in ("None", "", "null"): + return None + return val + + @app.get("/items/") -async def read_items(q: Annotated[Union[str, None], Query(min_length=3)]): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results +async def read_items( + q: Annotated[Optional[str], Query(min_length=3), BeforeValidator(nullable_str)], +): + return {"q": q} diff --git a/docs_src/query_params_str_validations/tutorial006c_py310.py b/docs_src/query_params_str_validations/tutorial006c_py310.py deleted file mode 100644 index 88b499c7a..000000000 --- a/docs_src/query_params_str_validations/tutorial006c_py310.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: str | None = Query(min_length=3)): - results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py deleted file mode 100644 index 344cf38e5..000000000 --- a/docs_src/query_params_str_validations/tutorial006d_an_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Optional, Union - -from fastapi import FastAPI, Query -from pydantic import BeforeValidator -from typing_extensions import Annotated - -app = FastAPI() - - -def nullable_str(val: str) -> Union[str, None]: - if val in ("None", "", "null"): - return None - return val - - -@app.get("/items/") -async def read_items( - q: Annotated[Optional[str], Query(min_length=3), BeforeValidator(nullable_str)], -): - return {"q": q} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py similarity index 51% rename from tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py rename to tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py index 86c13cd2e..b87443945 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py @@ -1,14 +1,26 @@ +import importlib + import pytest from fastapi.testclient import TestClient from tests.utils import needs_pydanticv2 +from ...utils import needs_py39, needs_py310 -@pytest.fixture -def client(): - from docs_src.query_params_str_validations.tutorial006d_an_py310 import app - yield TestClient(app) +@pytest.fixture( + name="client", + params=[ + "tutorial006c_an", + pytest.param("tutorial006c_an_py310", marks=needs_py310), + pytest.param("tutorial006c_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.query_params_str_validations.{request.param}" + ) + yield TestClient(mod.app) @needs_pydanticv2 From 03ca4c106d2733a35cd3f2aa098c9fd778c7fab8 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Fri, 28 Nov 2025 11:19:33 +0100 Subject: [PATCH 16/16] Update includes in em and zh version of page --- docs/em/docs/tutorial/query-params-str-validations.md | 2 +- docs/zh/docs/tutorial/query-params-str-validations.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/em/docs/tutorial/query-params-str-validations.md b/docs/em/docs/tutorial/query-params-str-validations.md index fd077bf8f..5cfce1a7c 100644 --- a/docs/em/docs/tutorial/query-params-str-validations.md +++ b/docs/em/docs/tutorial/query-params-str-validations.md @@ -154,7 +154,7 @@ q: Union[str, None] = Query(default=None, min_length=3) 👈, 👆 💪 📣 👈 `None` ☑ 🆎 ✋️ ⚙️ `default=...`: -{* ../../docs_src/query_params_str_validations/tutorial006c.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} /// tip diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md index c2f9a7e9f..50a51f184 100644 --- a/docs/zh/docs/tutorial/query-params-str-validations.md +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -114,7 +114,7 @@ q: Union[str, None] = Query(default=None, min_length=3) 为此,你可以声明`None`是一个有效的类型,并仍然使用`default=...`: -{* ../../docs_src/query_params_str_validations/tutorial006c.py hl[9] *} +{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} /// tip