]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix handling arbitrary types when using `arbitrary_types_allowed=True` (#14482)
authorSebastián Ramírez <tiangolo@gmail.com>
Wed, 10 Dec 2025 10:36:29 +0000 (02:36 -0800)
committerGitHub <noreply@github.com>
Wed, 10 Dec 2025 10:36:29 +0000 (11:36 +0100)
fastapi/_compat/v2.py
tests/test_arbitrary_types.py [new file with mode: 0644]

index acd23d8465f98da39300dae845ba86871e57cd45..46a30b3ee80d4d1f0c1385a8fe2f59a2cdda25a1 100644 (file)
@@ -1,7 +1,7 @@
 import re
 import warnings
 from copy import copy, deepcopy
-from dataclasses import dataclass
+from dataclasses import dataclass, is_dataclass
 from enum import Enum
 from typing import (
     Any,
@@ -18,7 +18,7 @@ from typing import (
 from fastapi._compat import may_v1, shared
 from fastapi.openapi.constants import REF_TEMPLATE
 from fastapi.types import IncEx, ModelNameMap, UnionType
-from pydantic import BaseModel, TypeAdapter, create_model
+from pydantic import BaseModel, ConfigDict, TypeAdapter, create_model
 from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
 from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
 from pydantic import ValidationError as ValidationError
@@ -64,6 +64,7 @@ class ModelField:
     field_info: FieldInfo
     name: str
     mode: Literal["validation", "serialization"] = "validation"
+    config: Union[ConfigDict, None] = None
 
     @property
     def alias(self) -> str:
@@ -94,8 +95,14 @@ class ModelField:
                 warnings.simplefilter(
                     "ignore", category=UnsupportedFieldAttributeWarning
                 )
+            annotated_args = (
+                self.field_info.annotation,
+                *self.field_info.metadata,
+                self.field_info,
+            )
             self._type_adapter: TypeAdapter[Any] = TypeAdapter(
-                Annotated[self.field_info.annotation, self.field_info]
+                Annotated[annotated_args],
+                config=self.config,
             )
 
     def get_default(self) -> Any:
@@ -412,10 +419,21 @@ def create_body_model(
 
 
 def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
-    return [
-        ModelField(field_info=field_info, name=name)
-        for name, field_info in model.model_fields.items()
-    ]
+    model_fields: List[ModelField] = []
+    for name, field_info in model.model_fields.items():
+        type_ = field_info.annotation
+        if lenient_issubclass(type_, (BaseModel, dict)) or is_dataclass(type_):
+            model_config = None
+        else:
+            model_config = model.model_config
+        model_fields.append(
+            ModelField(
+                field_info=field_info,
+                name=name,
+                config=model_config,
+            )
+        )
+    return model_fields
 
 
 # Duplicate of several schema functions from Pydantic v1 to make them compatible with
diff --git a/tests/test_arbitrary_types.py b/tests/test_arbitrary_types.py
new file mode 100644 (file)
index 0000000..e5fa95e
--- /dev/null
@@ -0,0 +1,141 @@
+from typing import List
+
+import pytest
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from typing_extensions import Annotated
+
+from .utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from pydantic import (
+        BaseModel,
+        ConfigDict,
+        PlainSerializer,
+        TypeAdapter,
+        WithJsonSchema,
+    )
+
+    class FakeNumpyArray:
+        def __init__(self):
+            self.data = [1.0, 2.0, 3.0]
+
+    FakeNumpyArrayPydantic = Annotated[
+        FakeNumpyArray,
+        WithJsonSchema(TypeAdapter(List[float]).json_schema()),
+        PlainSerializer(lambda v: v.data),
+    ]
+
+    class MyModel(BaseModel):
+        model_config = ConfigDict(arbitrary_types_allowed=True)
+        custom_field: FakeNumpyArrayPydantic
+
+    app = FastAPI()
+
+    @app.get("/")
+    def test() -> MyModel:
+        return MyModel(custom_field=FakeNumpyArray())
+
+    client = TestClient(app)
+    return client
+
+
+@needs_pydanticv2
+def test_get(client: TestClient):
+    response = client.get("/")
+    assert response.json() == {"custom_field": [1.0, 2.0, 3.0]}
+
+
+@needs_pydanticv2
+def test_typeadapter():
+    # This test is only to confirm that Pydantic alone is working as expected
+    from pydantic import (
+        BaseModel,
+        ConfigDict,
+        PlainSerializer,
+        TypeAdapter,
+        WithJsonSchema,
+    )
+
+    class FakeNumpyArray:
+        def __init__(self):
+            self.data = [1.0, 2.0, 3.0]
+
+    FakeNumpyArrayPydantic = Annotated[
+        FakeNumpyArray,
+        WithJsonSchema(TypeAdapter(List[float]).json_schema()),
+        PlainSerializer(lambda v: v.data),
+    ]
+
+    class MyModel(BaseModel):
+        model_config = ConfigDict(arbitrary_types_allowed=True)
+        custom_field: FakeNumpyArrayPydantic
+
+    ta = TypeAdapter(MyModel)
+    assert ta.dump_python(MyModel(custom_field=FakeNumpyArray())) == {
+        "custom_field": [1.0, 2.0, 3.0]
+    }
+    assert ta.json_schema() == snapshot(
+        {
+            "properties": {
+                "custom_field": {
+                    "items": {"type": "number"},
+                    "title": "Custom Field",
+                    "type": "array",
+                }
+            },
+            "required": ["custom_field"],
+            "title": "MyModel",
+            "type": "object",
+        }
+    )
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+    response = client.get("openapi.json")
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/": {
+                    "get": {
+                        "summary": "Test",
+                        "operationId": "test__get",
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/MyModel"
+                                        }
+                                    }
+                                },
+                            }
+                        },
+                    }
+                }
+            },
+            "components": {
+                "schemas": {
+                    "MyModel": {
+                        "properties": {
+                            "custom_field": {
+                                "items": {"type": "number"},
+                                "type": "array",
+                                "title": "Custom Field",
+                            }
+                        },
+                        "type": "object",
+                        "required": ["custom_field"],
+                        "title": "MyModel",
+                    }
+                }
+            },
+        }
+    )