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, ConfigDict, TypeAdapter, create_model
+from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
from pydantic import ValidationError as ValidationError
evaluate_forwardref = eval_type_lenient
Validator = Any
+# TODO: remove when dropping support for Pydantic < v2.12.3
+_Attrs = {
+ "default": ...,
+ "default_factory": None,
+ "alias": None,
+ "alias_priority": None,
+ "validation_alias": None,
+ "serialization_alias": None,
+ "title": None,
+ "field_title_generator": None,
+ "description": None,
+ "examples": None,
+ "exclude": None,
+ "exclude_if": None,
+ "discriminator": None,
+ "deprecated": None,
+ "json_schema_extra": None,
+ "frozen": None,
+ "validate_default": None,
+ "repr": True,
+ "init": None,
+ "init_var": None,
+ "kw_only": None,
+}
+
+
+# TODO: remove when dropping support for Pydantic < v2.12.3
+def asdict(field_info: FieldInfo) -> Dict[str, Any]:
+ attributes = {}
+ for attr in _Attrs:
+ value = getattr(field_info, attr, Undefined)
+ if value is not Undefined:
+ attributes[attr] = value
+ return {
+ "annotation": field_info.annotation,
+ "metadata": field_info.metadata,
+ "attributes": attributes,
+ }
+
class BaseConfig:
pass
warnings.simplefilter(
"ignore", category=UnsupportedFieldAttributeWarning
)
+ # TODO: remove after dropping support for Python 3.8 and
+ # setting the min Pydantic to v2.12.3 that adds asdict()
+ field_dict = asdict(self.field_info)
annotated_args = (
- self.field_info.annotation,
- *self.field_info.metadata,
- self.field_info,
+ field_dict["annotation"],
+ *field_dict["metadata"],
+ # this FieldInfo needs to be created again so that it doesn't include
+ # the old field info metadata and only the rest of the attributes
+ Field(**field_dict["attributes"]),
)
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[annotated_args],
--- /dev/null
+# Ref: https://github.com/fastapi/fastapi/discussions/14495
+
+from typing import Union
+
+import pytest
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from pydantic import BaseModel
+from typing_extensions import Annotated
+
+from .utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def client_fixture() -> TestClient:
+ from fastapi import Body
+ from pydantic import Discriminator, Tag
+
+ class Cat(BaseModel):
+ pet_type: str = "cat"
+ meows: int
+
+ class Dog(BaseModel):
+ pet_type: str = "dog"
+ barks: float
+
+ def get_pet_type(v):
+ assert isinstance(v, dict)
+ return v.get("pet_type", "")
+
+ Pet = Annotated[
+ Union[Annotated[Cat, Tag("cat")], Annotated[Dog, Tag("dog")]],
+ Discriminator(get_pet_type),
+ ]
+
+ app = FastAPI()
+
+ @app.post("/pet/assignment")
+ async def create_pet_assignment(pet: Pet = Body()):
+ return pet
+
+ @app.post("/pet/annotated")
+ async def create_pet_annotated(pet: Annotated[Pet, Body()]):
+ return pet
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+def test_union_body_discriminator_assignment(client: TestClient) -> None:
+ response = client.post("/pet/assignment", json={"pet_type": "cat", "meows": 5})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"pet_type": "cat", "meows": 5}
+
+
+@needs_pydanticv2
+def test_union_body_discriminator_annotated(client: TestClient) -> None:
+ response = client.post("/pet/annotated", json={"pet_type": "dog", "barks": 3.5})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"pet_type": "dog", "barks": 3.5}
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient) -> None:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == snapshot(
+ {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/pet/assignment": {
+ "post": {
+ "summary": "Create Pet Assignment",
+ "operationId": "create_pet_assignment_pet_assignment_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {"$ref": "#/components/schemas/Cat"},
+ {"$ref": "#/components/schemas/Dog"},
+ ],
+ "title": "Pet",
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/pet/annotated": {
+ "post": {
+ "summary": "Create Pet Annotated",
+ "operationId": "create_pet_annotated_pet_annotated_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {"$ref": "#/components/schemas/Cat"},
+ {"$ref": "#/components/schemas/Dog"},
+ ],
+ "title": "Pet",
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Cat": {
+ "properties": {
+ "pet_type": {
+ "type": "string",
+ "title": "Pet Type",
+ "default": "cat",
+ },
+ "meows": {"type": "integer", "title": "Meows"},
+ },
+ "type": "object",
+ "required": ["meows"],
+ "title": "Cat",
+ },
+ "Dog": {
+ "properties": {
+ "pet_type": {
+ "type": "string",
+ "title": "Pet Type",
+ "default": "dog",
+ },
+ "barks": {"type": "number", "title": "Barks"},
+ },
+ "type": "object",
+ "required": ["barks"],
+ "title": "Dog",
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
+ )