diff --git a/fastapi/encoders.py b/fastapi/encoders.py index c9f882d2ba..456742679b 100644 --- a/fastapi/encoders.py +++ b/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", diff --git a/fastapi/routing.py b/fastapi/routing.py index 470f506702..b3b70a9a1e 100644 --- a/fastapi/routing.py +++ b/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( diff --git a/tests/test_sse.py b/tests/test_sse.py index 6dfec61838..d604632281 100644 --- a/tests/test_sse.py +++ b/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