Browse Source

15498: Enable Pydantic polymorphic_serialization flag on response serialization

pull/15499/head
Valentyn Druzhynin 4 weeks ago
parent
commit
c398e26a56
  1. 54
      fastapi/_compat/v2.py
  2. 148
      fastapi/applications.py
  3. 157
      fastapi/routing.py
  4. 214
      tests/test_response_model_polymorphic_serialization.py

54
fastapi/_compat/v2.py

@ -198,19 +198,27 @@ class ModelField:
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
polymorphic_serialization: bool = False,
) -> Any:
# What calls this code passes a value that already called
# self._type_adapter.validate_python(value)
return self._type_adapter.dump_python(
value,
mode=mode,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
if polymorphic_serialization and shared.PYDANTIC_VERSION_MINOR_TUPLE < (2, 13): # pragma: no cover
raise ValueError(
"polymorphic_serialization requires Pydantic >= 2.13. "
f"Current version: {shared.PYDANTIC_VERSION}" # type: ignore[attr-defined] # ty: ignore[unused-ignore-comment]
)
kwargs = {
"mode": mode,
"include": include,
"exclude": exclude,
"by_alias": by_alias,
"exclude_unset": exclude_unset,
"exclude_defaults": exclude_defaults,
"exclude_none": exclude_none,
}
if shared.PYDANTIC_VERSION_MINOR_TUPLE >= (2, 13):
kwargs["polymorphic_serialization"] = polymorphic_serialization
return self._type_adapter.dump_python(value, **kwargs) # type: ignore[arg-type]
def serialize_json(
self,
@ -222,21 +230,29 @@ class ModelField:
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
polymorphic_serialization: bool = False,
) -> bytes:
# What calls this code passes a value that already called
# self._type_adapter.validate_python(value)
# This uses Pydantic's dump_json() which serializes directly to JSON
# bytes in one pass (via Rust), avoiding the intermediate Python dict
# step of dump_python(mode="json") + json.dumps().
return self._type_adapter.dump_json(
value,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
if polymorphic_serialization and shared.PYDANTIC_VERSION_MINOR_TUPLE < (2, 13): # pragma: no cover
raise ValueError(
"polymorphic_serialization requires Pydantic >= 2.13. "
f"Current version: {shared.PYDANTIC_VERSION}" # type: ignore[attr-defined] # ty: ignore[unused-ignore-comment]
)
kwargs = {
"include": include,
"exclude": exclude,
"by_alias": by_alias,
"exclude_unset": exclude_unset,
"exclude_defaults": exclude_defaults,
"exclude_none": exclude_none,
}
if shared.PYDANTIC_VERSION_MINOR_TUPLE >= (2, 13):
kwargs["polymorphic_serialization"] = polymorphic_serialization
return self._type_adapter.dump_json(value, **kwargs) # type: ignore[arg-type]
def __hash__(self) -> int:
# Each ModelField is unique for our purposes, to allow making a dict from

148
fastapi/applications.py

@ -1180,6 +1180,7 @@ class FastAPI(Starlette):
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
response_model_polymorphic_serialization: bool = False,
include_in_schema: bool = True,
response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),
name: str | None = None,
@ -1208,6 +1209,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -1236,6 +1238,7 @@ class FastAPI(Starlette):
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
response_model_polymorphic_serialization: bool = False,
include_in_schema: bool = True,
response_class: type[Response] = Default(JSONResponse),
name: str | None = None,
@ -1265,6 +1268,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -1814,6 +1818,23 @@ class FastAPI(Starlette):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -1925,6 +1946,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -2187,6 +2209,23 @@ class FastAPI(Starlette):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -2303,6 +2342,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -2565,6 +2605,23 @@ class FastAPI(Starlette):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -2681,6 +2738,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -2943,6 +3001,23 @@ class FastAPI(Starlette):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -3054,6 +3129,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -3316,6 +3392,23 @@ class FastAPI(Starlette):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -3427,6 +3520,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -3689,6 +3783,23 @@ class FastAPI(Starlette):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -3800,6 +3911,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -4062,6 +4174,23 @@ class FastAPI(Starlette):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -4178,6 +4307,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -4440,6 +4570,23 @@ class FastAPI(Starlette):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -4551,6 +4698,7 @@ class FastAPI(Starlette):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,

157
fastapi/routing.py

@ -284,6 +284,7 @@ async def serialize_response(
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
polymorphic_serialization: bool = False,
is_coroutine: bool = True,
endpoint_ctx: EndpointContext | None = None,
dump_json: bool = False,
@ -311,6 +312,7 @@ async def serialize_response(
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
polymorphic_serialization=polymorphic_serialization,
)
else:
@ -360,6 +362,7 @@ def get_request_handler(
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
response_model_polymorphic_serialization: bool = False,
dependency_overrides_provider: Any | None = None,
embed_body_fields: bool = False,
strict_content_type: bool | DefaultPlaceholder = Default(True),
@ -488,6 +491,7 @@ def get_request_handler(
exclude_unset=response_model_exclude_unset,
exclude_defaults=response_model_exclude_defaults,
exclude_none=response_model_exclude_none,
polymorphic_serialization=response_model_polymorphic_serialization,
)
else:
data = jsonable_encoder(data)
@ -701,6 +705,7 @@ def get_request_handler(
exclude_unset=response_model_exclude_unset,
exclude_defaults=response_model_exclude_defaults,
exclude_none=response_model_exclude_none,
polymorphic_serialization=response_model_polymorphic_serialization,
is_coroutine=is_coroutine,
endpoint_ctx=endpoint_ctx,
dump_json=use_dump_json,
@ -832,6 +837,7 @@ class APIRoute(routing.Route):
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
response_model_polymorphic_serialization: bool = False,
include_in_schema: bool = True,
response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),
dependency_overrides_provider: Any | None = None,
@ -876,6 +882,9 @@ class APIRoute(routing.Route):
self.response_model_exclude_unset = response_model_exclude_unset
self.response_model_exclude_defaults = response_model_exclude_defaults
self.response_model_exclude_none = response_model_exclude_none
self.response_model_polymorphic_serialization = (
response_model_polymorphic_serialization
)
self.include_in_schema = include_in_schema
self.response_class = response_class
self.dependency_overrides_provider = dependency_overrides_provider
@ -988,6 +997,7 @@ class APIRoute(routing.Route):
response_model_exclude_unset=self.response_model_exclude_unset,
response_model_exclude_defaults=self.response_model_exclude_defaults,
response_model_exclude_none=self.response_model_exclude_none,
response_model_polymorphic_serialization=self.response_model_polymorphic_serialization,
dependency_overrides_provider=self.dependency_overrides_provider,
embed_body_fields=self._embed_body_fields,
strict_content_type=self.strict_content_type,
@ -1355,6 +1365,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
response_model_polymorphic_serialization: bool = False,
include_in_schema: bool = True,
response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),
name: str | None = None,
@ -1403,6 +1414,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema and self.include_in_schema,
response_class=current_response_class,
name=name,
@ -1437,6 +1449,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
response_model_polymorphic_serialization: bool = False,
include_in_schema: bool = True,
response_class: type[Response] = Default(JSONResponse),
name: str | None = None,
@ -1467,6 +1480,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -2082,6 +2096,23 @@ class APIRouter(routing.Router):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -2197,6 +2228,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -2459,6 +2491,23 @@ class APIRouter(routing.Router):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -2579,6 +2628,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -2841,6 +2891,23 @@ class APIRouter(routing.Router):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -2961,6 +3028,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -3223,6 +3291,23 @@ class APIRouter(routing.Router):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -3338,6 +3423,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -3600,6 +3686,23 @@ class APIRouter(routing.Router):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -3715,6 +3818,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -3977,6 +4081,23 @@ class APIRouter(routing.Router):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -4097,6 +4218,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -4359,6 +4481,23 @@ class APIRouter(routing.Router):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(
@ -4479,6 +4618,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
response_model_polymorphic_serialization=response_model_polymorphic_serialization,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -4741,6 +4881,23 @@ class APIRouter(routing.Router):
"""
),
] = False,
response_model_polymorphic_serialization: Annotated[
bool,
Doc(
"""
Configuration passed to Pydantic to enable polymorphic serialization.
When `True`, if you return a subclass instance through a base-class-typed
response model, the serialized response will include fields from the
subclass. Requires Pydantic >= 2.13.
When `False` (default), only base class fields are included in the response.
Read more about it in the
[Pydantic docs for Polymorphic Serialization](https://docs.pydantic.dev/latest/concepts/serialization/#subclass-instances-for-fields-of-baseclass-type).
"""
),
] = False,
include_in_schema: Annotated[
bool,
Doc(

214
tests/test_response_model_polymorphic_serialization.py

@ -0,0 +1,214 @@
from collections.abc import Iterator
import pytest
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel
class User(BaseModel):
name: str
class UserLogin(User):
password: str
class OuterModel(BaseModel):
user: User
class Item(BaseModel):
name: str
description: str
class ItemWithPrice(Item):
price: float
requires_pydantic_v2_13 = pytest.mark.skipif(
PYDANTIC_VERSION_MINOR_TUPLE < (2, 13),
reason="polymorphic_serialization requires Pydantic >= 2.13",
)
only_for_pydantic_below_v2_13 = pytest.mark.skipif(
PYDANTIC_VERSION_MINOR_TUPLE >= (2, 13),
reason="This test only applies to Pydantic < 2.13",
)
requires_pydantic_v2_13 = pytest.mark.skipif(
PYDANTIC_VERSION_MINOR_TUPLE < (2, 13),
reason="polymorphic_serialization requires Pydantic >= 2.13",
)
only_for_pydantic_below_v2_13 = pytest.mark.skipif(
PYDANTIC_VERSION_MINOR_TUPLE >= (2, 13),
reason="This test only applies to Pydantic < 2.13",
)
def test_default_behavior_filters_subclass_fields():
app = FastAPI()
@app.get("/user", response_model=OuterModel)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic"}}
@requires_pydantic_v2_13
def test_polymorphic_flag_includes_subclass_fields():
app = FastAPI()
@app.get(
"/user",
response_model=OuterModel,
response_model_polymorphic_serialization=True,
)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
@requires_pydantic_v2_13
def test_polymorphic_with_inferred_response_model():
app = FastAPI()
@app.get("/user", response_model_polymorphic_serialization=True)
def get_user() -> OuterModel:
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
@requires_pydantic_v2_13
def test_polymorphic_with_fast_json_path():
app = FastAPI()
@app.post(
"/item",
response_model=Item,
response_model_polymorphic_serialization=True,
status_code=201,
)
def create_item() -> Item:
return ItemWithPrice(name="Widget", description="A useful widget", price=9.99)
response = TestClient(app).post("/item")
assert response.status_code == 201
assert response.json() == {
"name": "Widget",
"description": "A useful widget",
"price": 9.99,
}
@requires_pydantic_v2_13
def test_polymorphic_with_custom_response_class():
app = FastAPI()
class CustomJSONResponse(JSONResponse):
media_type = "application/json"
@app.get(
"/user",
response_model=OuterModel,
response_model_polymorphic_serialization=True,
response_class=CustomJSONResponse,
)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
@requires_pydantic_v2_13
def test_polymorphic_composes_with_exclude():
app = FastAPI()
@app.get(
"/user",
response_model=OuterModel,
response_model_polymorphic_serialization=True,
response_model_exclude={"user": {"password"}},
)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic"}}
def test_list_response_filters_subclass_fields_by_default():
app = FastAPI()
def generate_items() -> Iterator[Item]:
yield ItemWithPrice(name="Item1", description="First item", price=10.0)
@app.get("/items/stream", response_model=list[Item])
def stream_items():
return list(generate_items())
response = TestClient(app).get("/items/stream")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0] == {"name": "Item1", "description": "First item"}
@only_for_pydantic_below_v2_13
def test_raises_error_for_unsupported_pydantic_version():
app = FastAPI()
@app.get(
"/user",
response_model=OuterModel,
response_model_polymorphic_serialization=True,
)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
with pytest.raises(ValueError) as exc_info:
TestClient(app).get("/user")
assert "polymorphic_serialization requires Pydantic >= 2.13" in str(exc_info.value)
def test_flag_defaults_to_false():
app = FastAPI()
@app.get("/user1", response_model=OuterModel)
def get_user1():
return OuterModel(user=UserLogin(name="user1", password="pass1"))
@app.post("/user2", response_model=OuterModel)
def post_user2():
return OuterModel(user=UserLogin(name="user2", password="pass2"))
client = TestClient(app)
response1 = client.get("/user1")
assert response1.json() == {"user": {"name": "user1"}}
response2 = client.post("/user2")
assert response2.json() == {"user": {"name": "user2"}}
Loading…
Cancel
Save