From 9ac56c70f214fd02fc82eccaf4c7da3dfa380f9c Mon Sep 17 00:00:00 2001 From: rmawatson Date: Sat, 20 Sep 2025 19:25:53 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Reenable=20`allow=5Farbitrary=5F?= =?UTF-8?q?types`=20when=20only=201=20argument=20is=20used=20on=20the=20AP?= =?UTF-8?q?I=20endpoint=20(#13694)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- fastapi/dependencies/utils.py | 6 ++++- tests/test_compat.py | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 1b15e6459..d9f6bf2d7 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -922,7 +922,11 @@ async def request_body_to_args( fields_to_extract: List[ModelField] = body_fields - if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel): + if ( + single_not_embedded_field + and lenient_issubclass(first_field.type_, BaseModel) + and isinstance(received_body, FormData) + ): fields_to_extract = get_cached_model_fields(first_field.type_) if isinstance(received_body, FormData): diff --git a/tests/test_compat.py b/tests/test_compat.py index f4a3093c5..43c686489 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -80,6 +80,51 @@ def test_complex(): assert response2.json() == [1, 2] +@needs_pydanticv2 +def test_propagates_pydantic2_model_config(): + app = FastAPI() + + class Missing: + def __bool__(self): + return False + + class EmbeddedModel(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + value: Union[str, Missing] = Missing() + + class Model(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + ) + value: Union[str, Missing] = Missing() + embedded_model: EmbeddedModel = EmbeddedModel() + + @app.post("/") + def foo(req: Model) -> Dict[str, Union[str, None]]: + return { + "value": req.value or None, + "embedded_value": req.embedded_model.value or None, + } + + client = TestClient(app) + + response = client.post("/", json={}) + assert response.status_code == 200, response.text + assert response.json() == { + "value": None, + "embedded_value": None, + } + + response2 = client.post( + "/", json={"value": "foo", "embedded_model": {"value": "bar"}} + ) + assert response2.status_code == 200, response2.text + assert response2.json() == { + "value": "foo", + "embedded_value": "bar", + } + + def test_is_bytes_sequence_annotation_union(): # For coverage # TODO: in theory this would allow declaring types that could be lists of bytes