Browse Source

Optimize response serialization using Pydantic v2 TypeAdapter

This commit introduces a fast-path for the jsonable_encoder and directly uses TypeAdapter(Any).dump_json() and dump_python() in astapi.routing and astapi.encoders. By leveraging the Rust-based pydantic-core directly, serialization overhead for complex types is significantly reduced.

- Use TypeAdapter(Any).dump_json() in _serialize_data and _serialize_sse_item

- Add fast-path to jsonable_encoder to use TypeAdapter(Any).dump_python(mode='json')

- Update test_sse_events_with_fields to account for minified JSON output of Pydantic v2
pull/15584/head
valbort 3 weeks ago
parent
commit
22aee4522f
  1. 20
      fastapi/encoders.py
  2. 8
      fastapi/routing.py
  3. 2
      tests/test_sse.py

20
fastapi/encoders.py

@ -21,7 +21,7 @@ from uuid import UUID
from annotated_doc import Doc from annotated_doc import Doc
from fastapi.exceptions import PydanticV1NotSupportedError from fastapi.exceptions import PydanticV1NotSupportedError
from fastapi.types import IncEx from fastapi.types import IncEx
from pydantic import BaseModel from pydantic import BaseModel, TypeAdapter
from pydantic.networks import AnyUrl, NameEmail from pydantic.networks import AnyUrl, NameEmail
from pydantic.types import SecretBytes, SecretStr from pydantic.types import SecretBytes, SecretStr
from pydantic_core import PydanticUndefinedType from pydantic_core import PydanticUndefinedType
@ -125,6 +125,8 @@ def generate_encoders_by_class_tuples(
encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE) encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
_any_type_adapter = TypeAdapter(Any)
def jsonable_encoder( def jsonable_encoder(
obj: Annotated[ obj: Annotated[
@ -240,6 +242,22 @@ def jsonable_encoder(
include = set(include) # type: ignore[assignment] # ty: ignore[invalid-assignment] include = set(include) # type: ignore[assignment] # ty: ignore[invalid-assignment]
if exclude is not None and not isinstance(exclude, (set, dict)): if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude) # type: ignore[assignment] # ty: ignore[invalid-assignment] exclude = set(exclude) # type: ignore[assignment] # ty: ignore[invalid-assignment]
if not custom_encoder and not sqlalchemy_safe:
try:
return _any_type_adapter.dump_python(
obj,
mode="json",
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
except Exception:
pass
if isinstance(obj, BaseModel): if isinstance(obj, BaseModel):
obj_dict = obj.model_dump( obj_dict = obj.model_dump(
mode="json", mode="json",

8
fastapi/routing.py

@ -91,6 +91,9 @@ from starlette.routing import Mount as Mount # noqa
from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from typing_extensions import deprecated from typing_extensions import deprecated
from pydantic import TypeAdapter
_any_type_adapter = TypeAdapter(Any)
# Copy of starlette.routing.request_response modified to include the # Copy of starlette.routing.request_response modified to include the
@ -493,8 +496,7 @@ def get_request_handler(
exclude_none=response_model_exclude_none, exclude_none=response_model_exclude_none,
) )
else: else:
data = jsonable_encoder(data) return _any_type_adapter.dump_json(data)
return json.dumps(data).encode("utf-8")
if is_sse_stream: if is_sse_stream:
# Generator endpoint: stream as Server-Sent Events # Generator endpoint: stream as Server-Sent Events
@ -512,7 +514,7 @@ def get_request_handler(
if hasattr(item.data, "model_dump_json"): if hasattr(item.data, "model_dump_json"):
data_str = item.data.model_dump_json() data_str = item.data.model_dump_json()
else: else:
data_str = json.dumps(jsonable_encoder(item.data)) data_str = _any_type_adapter.dump_json(item.data).decode("utf-8")
else: else:
data_str = None data_str = None
return format_sse_event( return format_sse_event(

2
tests/test_sse.py

@ -191,7 +191,7 @@ def test_sse_events_with_fields(client: TestClient):
assert "event: json-data\n" in text assert "event: json-data\n" in text
assert "id: 2\n" in text assert "id: 2\n" in text
assert 'data: {"key": "value"}\n' in text assert 'data: {"key":"value"}\n' in text
assert ": just a comment\n" in text assert ": just a comment\n" in text

Loading…
Cancel
Save