From 7981889e60fcf9a6eb5564a281b642899c36128f Mon Sep 17 00:00:00 2001 From: Krzysztof Kosyl Date: Wed, 22 Mar 2023 02:04:58 +0100 Subject: [PATCH 1/4] Allow duplicated variable names in dependency --- fastapi/dependencies/utils.py | 2 +- ...est_dependency_body_duplicated_variable.py | 126 ++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/test_dependency_body_duplicated_variable.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index c581348c9..519110d32 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -816,7 +816,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: model_name = "Body_" + name BodyModel: Type[BaseModel] = create_model(model_name) for f in flat_dependant.body_params: - BodyModel.__fields__[f.name] = f + BodyModel.__fields__[f.alias] = f required = any(True for f in flat_dependant.body_params if f.required) BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None} diff --git a/tests/test_dependency_body_duplicated_variable.py b/tests/test_dependency_body_duplicated_variable.py new file mode 100644 index 000000000..ad64438a7 --- /dev/null +++ b/tests/test_dependency_body_duplicated_variable.py @@ -0,0 +1,126 @@ +from typing import Awaitable, Callable, List +from unittest.mock import ANY + +import pytest +from fastapi import Body, Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +def make_field(name: str) -> Callable[..., Awaitable[str]]: + async def inner(value: str = Body(..., alias=name)) -> str: + return value + + return inner + + +@app.post("/example") +def example( + field_0: str = Body(...), + _field_1: str = Body(..., alias="field_1"), + _field_2: str = Depends(make_field("field_2")), + _field_3: str = Depends(make_field("field_3")), +) -> List[str]: + return [field_0, _field_1, _field_2, _field_3] + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/example": { + "post": { + "summary": "Example", + "operationId": "example_example_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_example_example_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Example Example Post", + "type": "array", + "items": {"type": "string"}, + } + } + }, + }, + "422": ANY, + }, + } + } + }, + "components": { + "schemas": { + "Body_example_example_post": { + "title": "Body_example_example_post", + "type": "object", + "properties": { + "field_0": {"title": "Field 0", "type": "string"}, + "field_1": {"title": "Field 1", "type": "string"}, + "field_2": {"title": "Field 2", "type": "string"}, + "field_3": {"title": "Field 3", "type": "string"}, + }, + "required": ["field_0", "field_1", "field_2", "field_3"], + }, + "HTTPValidationError": ANY, + "ValidationError": ANY, + } + }, +} + + +client = TestClient(app) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def _field_missing(name): + return { + "loc": ["body", name], + "msg": "field required", + "type": "value_error.missing", + } + + +@pytest.mark.parametrize( + "body_json,expected_status,expected_response", + [ + [ + {}, + 422, + { + "detail": [ + _field_missing("field_2"), + _field_missing("field_3"), + _field_missing("field_0"), + _field_missing("field_1"), + ], + }, + ], + [ + {"field_0": "a", "field_1": "b", "field_2": "c", "field_3": "d"}, + 200, + ["a", "b", "c", "d"], + ], + ], +) +def test_endpoint(body_json, expected_status, expected_response): + response = client.post("/example/", json=body_json) + assert response.status_code == expected_status, response.text + assert response.json() == expected_response From f355a5e5821c62675ab58125468af07bf410038e Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 9 Jul 2025 21:49:04 +0200 Subject: [PATCH 2/4] Update tests to work with Pydantic V2 --- ...est_dependency_body_duplicated_variable.py | 67 ++++++++----------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/tests/test_dependency_body_duplicated_variable.py b/tests/test_dependency_body_duplicated_variable.py index ad64438a7..77ae8aace 100644 --- a/tests/test_dependency_body_duplicated_variable.py +++ b/tests/test_dependency_body_duplicated_variable.py @@ -1,7 +1,6 @@ from typing import Awaitable, Callable, List from unittest.mock import ANY -import pytest from fastapi import Body, Depends, FastAPI from fastapi.testclient import TestClient @@ -18,15 +17,15 @@ def make_field(name: str) -> Callable[..., Awaitable[str]]: @app.post("/example") def example( field_0: str = Body(...), - _field_1: str = Body(..., alias="field_1"), - _field_2: str = Depends(make_field("field_2")), - _field_3: str = Depends(make_field("field_3")), + field_1_: str = Body(..., alias="field_1"), + field_2_: str = Depends(make_field("field_2")), + field_3_: str = Depends(make_field("field_3")), ) -> List[str]: - return [field_0, _field_1, _field_2, _field_3] + return [field_0, field_1_, field_2_, field_3_] openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/example": { @@ -90,37 +89,25 @@ def test_openapi_schema(): assert response.json() == openapi_schema -def _field_missing(name): - return { - "loc": ["body", name], - "msg": "field required", - "type": "value_error.missing", - } - - -@pytest.mark.parametrize( - "body_json,expected_status,expected_response", - [ - [ - {}, - 422, - { - "detail": [ - _field_missing("field_2"), - _field_missing("field_3"), - _field_missing("field_0"), - _field_missing("field_1"), - ], - }, - ], - [ - {"field_0": "a", "field_1": "b", "field_2": "c", "field_3": "d"}, - 200, - ["a", "b", "c", "d"], - ], - ], -) -def test_endpoint(body_json, expected_status, expected_response): - response = client.post("/example/", json=body_json) - assert response.status_code == expected_status, response.text - assert response.json() == expected_response +def test_valid(): + response = client.post( + "/example/", + json={"field_0": "a", "field_1": "b", "field_2": "c", "field_3": "d"}, + ) + assert response.status_code == 200, response.text + assert response.json() == ["a", "b", "c", "d"] + + +def test_missing(): + response = client.post("/example/", json={}) + assert response.status_code == 422, response.text + resp_json = response.json() + assert len(resp_json["detail"]) == 4 + assert resp_json["detail"][0]["loc"] == ["body", "field_2"] + assert str(resp_json["detail"][0]["msg"]).lower() == "field required" + assert resp_json["detail"][1]["loc"] == ["body", "field_3"] + assert str(resp_json["detail"][1]["msg"]).lower() == "field required" + assert resp_json["detail"][2]["loc"] == ["body", "field_0"] + assert str(resp_json["detail"][2]["msg"]).lower() == "field required" + assert resp_json["detail"][3]["loc"] == ["body", "field_1"] + assert str(resp_json["detail"][3]["msg"]).lower() == "field required" From 3dd2024706febc6319373645b7ef861aed38a5b7 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 9 Jul 2025 21:49:29 +0200 Subject: [PATCH 3/4] Apply fix --- fastapi/_compat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 227ad837d..a335fc3ae 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -282,7 +282,7 @@ if PYDANTIC_V2: def create_body_model( *, fields: Sequence[ModelField], model_name: str ) -> Type[BaseModel]: - field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} + field_params = {f.alias: (f.field_info.annotation, f.field_info) for f in fields} BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] return BodyModel @@ -524,7 +524,7 @@ else: ) -> Type[BaseModel]: BodyModel = create_model(model_name) for f in fields: - BodyModel.__fields__[f.name] = f # type: ignore[index] + BodyModel.__fields__[f.alias] = f # type: ignore[index] return BodyModel def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: From 1a2304e88b965de3bf6b6e69dc249f9783b323d0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:51:10 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index a335fc3ae..ad35d4560 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -282,7 +282,9 @@ if PYDANTIC_V2: def create_body_model( *, fields: Sequence[ModelField], model_name: str ) -> Type[BaseModel]: - field_params = {f.alias: (f.field_info.annotation, f.field_info) for f in fields} + field_params = { + f.alias: (f.field_info.annotation, f.field_info) for f in fields + } BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] return BodyModel