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 2 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 fastapi.exceptions import PydanticV1NotSupportedError
from fastapi.types import IncEx
from pydantic import BaseModel
from pydantic import BaseModel, TypeAdapter
from pydantic.networks import AnyUrl, NameEmail
from pydantic.types import SecretBytes, SecretStr
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)
_any_type_adapter = TypeAdapter(Any)
def jsonable_encoder(
obj: Annotated[
@ -240,6 +242,22 @@ def jsonable_encoder(
include = set(include) # type: ignore[assignment] # ty: ignore[invalid-assignment]
if exclude is not None and not isinstance(exclude, (set, dict)):
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):
obj_dict = obj.model_dump(
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.websockets import WebSocket
from typing_extensions import deprecated
from pydantic import TypeAdapter
_any_type_adapter = TypeAdapter(Any)
# Copy of starlette.routing.request_response modified to include the
@ -493,8 +496,7 @@ def get_request_handler(
exclude_none=response_model_exclude_none,
)
else:
data = jsonable_encoder(data)
return json.dumps(data).encode("utf-8")
return _any_type_adapter.dump_json(data)
if is_sse_stream:
# Generator endpoint: stream as Server-Sent Events
@ -512,7 +514,7 @@ def get_request_handler(
if hasattr(item.data, "model_dump_json"):
data_str = item.data.model_dump_json()
else:
data_str = json.dumps(jsonable_encoder(item.data))
data_str = _any_type_adapter.dump_json(item.data).decode("utf-8")
else:
data_str = None
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 "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

Loading…
Cancel
Save