From 882fa913dac8bc192df86978814dd3623db6951e Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 10:32:46 +0530 Subject: [PATCH 01/17] 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 4b8cc9d297..10dafb67be 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -246,11 +246,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 6eb348433f99f67317e02bcb3b13a172008d3485 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 14:55:38 +0530 Subject: [PATCH 02/17] 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 10dafb67be..68e9a3d5f9 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -257,23 +257,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 0000000000..0962fbce7e --- /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 0000000000..57fccec947 --- /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 a28557e0c1d022427291ac8a2a5712b1b663a75f 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/17] =?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 68e9a3d5f9..e24c248ac3 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -257,7 +257,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 0962fbce7e..025d314ea2 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 57fccec947..36ae95f65c 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 9b8beb4700996a3833ce25568a631abdd3a23b1e Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 15:05:37 +0530 Subject: [PATCH 04/17] 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 025d314ea2..2fd8923bfe 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 f991cce9594e636dd84ccf39e9f31d73065b8579 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 15:45:23 +0530 Subject: [PATCH 05/17] 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 e24c248ac3..3365f7b8e1 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -257,12 +257,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 2fd8923bfe..9a9d082bf5 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 36ae95f65c..af7e9dd09f 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 d240ac8d124cab2100d696a612a11fa357189fa6 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/17] =?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 9a9d082bf5..94af457225 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 6c021d228d4f183fa7484ebf3abec853ecc59c2a Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 16:08:08 +0530 Subject: [PATCH 07/17] 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 94af457225..6e58cc8f0d 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 05f799973496c901e0a1cacd02df022081286a24 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/17] =?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 6e58cc8f0d..344cf38e53 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 b960384f3f715ccb2d89094548e45beb737fb907 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Mon, 10 Nov 2025 16:21:20 +0530 Subject: [PATCH 09/17] 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 af7e9dd09f..113ab2f93e 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 909d174fe99136e4c8dc5e8d50ffae9bb4f81b35 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/17] =?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 113ab2f93e..86c13cd2e9 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 79ba18b3dd20f01eed75f31746744558b8578d07 Mon Sep 17 00:00:00 2001 From: Chaitanya Sai Meka Date: Tue, 11 Nov 2025 16:31:32 +0530 Subject: [PATCH 11/17] 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 3365f7b8e1..6d04636016 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -264,6 +264,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 67c8518ce305776939a13d37dd2eddf664da803e 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/17] 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 6d04636016..07e6abbeaf 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -262,9 +262,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 d377cbf7b9d03c97035496d0b2a156e1775eb87d 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/17] 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 07e6abbeaf..40d712fa21 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -262,7 +262,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 98bc4dbab79105db1b9848392a6814f17d056678 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/17] 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 40d712fa21..8dd962d5aa 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -262,7 +262,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 1a4cdb35eb32e6edf5af4276b55e3f5bc5c5deed Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Fri, 28 Nov 2025 10:51:41 +0100 Subject: [PATCH 15/17] Rename code example file and add variants --- .../tutorial/query-params-str-validations.md | 2 +- .../tutorial006c_an_py310.py | 11 +++++++- .../tutorial006c_an_py39.py | 11 +++++++- .../tutorial006c_py310.py | 11 -------- .../tutorial006c_py39.py | 13 --------- .../tutorial006d_an_py310.py | 20 ------------- .../test_tutorial006c.py | 28 +++++++++---------- .../test_tutorial006d_an_py310.py | 27 ------------------ 8 files changed, 35 insertions(+), 88 deletions(-) delete mode 100644 docs_src/query_params_str_validations/tutorial006c_py310.py delete mode 100644 docs_src/query_params_str_validations/tutorial006c_py39.py delete mode 100644 docs_src/query_params_str_validations/tutorial006d_an_py310.py delete 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 8dd962d5aa..47003dab62 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -262,7 +262,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_an_py310.py b/docs_src/query_params_str_validations/tutorial006c_an_py310.py index 2995d9c979..d5c3d3058a 100644 --- a/docs_src/query_params_str_validations/tutorial006c_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial006c_an_py310.py @@ -1,12 +1,21 @@ 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)]): +async def read_items( + q: Annotated[str | None, Query(min_length=3), BeforeValidator(nullable_str)], +): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"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 76a1cd49ac..79516eb1d8 100644 --- a/docs_src/query_params_str_validations/tutorial006c_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial006c_an_py39.py @@ -1,12 +1,21 @@ from typing import Annotated, 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)]): +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3), BeforeValidator(nullable_str)], +): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"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 88b499c7af..0000000000 --- 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/tutorial006c_py39.py b/docs_src/query_params_str_validations/tutorial006c_py39.py deleted file mode 100644 index 0a0e820da3..0000000000 --- a/docs_src/query_params_str_validations/tutorial006c_py39.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/tutorial006d_an_py310.py b/docs_src/query_params_str_validations/tutorial006d_an_py310.py deleted file mode 100644 index 344cf38e53..0000000000 --- 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_tutorial006c.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py index f287b5dcd8..af1d74315b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py @@ -9,8 +9,6 @@ from ...utils import needs_py310 @pytest.fixture( name="client", params=[ - pytest.param("tutorial006c_py39"), - pytest.param("tutorial006c_py310", marks=needs_py310), pytest.param("tutorial006c_an_py39"), pytest.param("tutorial006c_an_py310", marks=needs_py310), ], @@ -23,24 +21,26 @@ def get_client(request: pytest.FixtureRequest): return client -@pytest.mark.xfail( - reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419" -) def test_query_params_str_validations_no_query(client: TestClient): response = client.get("/items/") - assert response.status_code == 200 - assert response.json() == { # pragma: no cover - "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + } + ] } -@pytest.mark.xfail( - reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419" -) -def test_query_params_str_validations_empty_str(client: TestClient): - response = client.get("/items/?q=") +@pytest.mark.parametrize("q_value", ["None", "null", ""]) +def test_query_params_str_validations_send_explicit_none(client: TestClient, q_value: str): + response = client.get("/items/", params={"q": q_value}) assert response.status_code == 200 - assert response.json() == { # pragma: no cover + assert response.json() == { "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], } 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 deleted file mode 100644 index 86c13cd2e9..0000000000 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006d_an_py310.py +++ /dev/null @@ -1,27 +0,0 @@ -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) - - -@needs_pydanticv2 -@pytest.mark.parametrize( - "q_value,expected", - [ - ("None", None), - ("", None), - ("null", None), - ("hello", "hello"), - ], -) -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 1de6fc107e7c23a36011eeef6cd835a5c9e14d5d 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, 8 Jan 2026 11:30:55 +0000 Subject: [PATCH 16/17] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_query_params_str_validations/test_tutorial006c.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py index af1d74315b..f7ae011b0a 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py @@ -37,7 +37,9 @@ def test_query_params_str_validations_no_query(client: TestClient): @pytest.mark.parametrize("q_value", ["None", "null", ""]) -def test_query_params_str_validations_send_explicit_none(client: TestClient, q_value: str): +def test_query_params_str_validations_send_explicit_none( + client: TestClient, q_value: str +): response = client.get("/items/", params={"q": q_value}) assert response.status_code == 200 assert response.json() == { From 9f6fe4a89461cf78f56a184211cb9d1ae8fd8900 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Thu, 8 Jan 2026 12:44:47 +0100 Subject: [PATCH 17/17] Update text in docs --- .../docs/tutorial/query-params-str-validations.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 47003dab62..82abae5301 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -246,9 +246,9 @@ 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 might want to declare that a parameter can accept `None`, but is still required. +You might want to declare a parameter that can accept `None` but is still required. -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**. +However, because of how **HTTP query parameters** work, clients can not 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. For example, you might try: @@ -257,19 +257,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 **request 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 tries to send `q=None`, `q=null` or `q=`, these values will be treated by FastAPI as strings `"None"`, `"null"` and `""` (empty string) respectively. 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/tutorial006c_an_py310.py hl[9:12,17] *} -/// 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.