Browse Source

Merge branch 'master' into no-coverage

pull/14971/head
Sebastián Ramírez 5 months ago
committed by GitHub
parent
commit
c7477ed866
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 26
      fastapi/_compat/v2.py
  2. 23
      fastapi/routing.py
  3. 51
      tests/test_dump_json_fast_path.py

26
fastapi/_compat/v2.py

@ -199,6 +199,32 @@ class ModelField:
exclude_none=exclude_none, exclude_none=exclude_none,
) )
def serialize_json(
self,
value: Any,
*,
include: IncEx | None = None,
exclude: IncEx | None = None,
by_alias: bool = True,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: 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,
)
def __hash__(self) -> int: def __hash__(self) -> int:
# Each ModelField is unique for our purposes, to allow making a dict from # Each ModelField is unique for our purposes, to allow making a dict from
# ModelField to its JSON Schema. # ModelField to its JSON Schema.

23
fastapi/routing.py

@ -271,6 +271,7 @@ async def serialize_response(
exclude_none: bool = False, exclude_none: bool = False,
is_coroutine: bool = True, is_coroutine: bool = True,
endpoint_ctx: EndpointContext | None = None, endpoint_ctx: EndpointContext | None = None,
dump_json: bool = False,
) -> Any: ) -> Any:
if field: if field:
if is_coroutine: if is_coroutine:
@ -286,8 +287,8 @@ async def serialize_response(
body=response_content, body=response_content,
endpoint_ctx=ctx, endpoint_ctx=ctx,
) )
serializer = field.serialize_json if dump_json else field.serialize
return field.serialize( return serializer(
value, value,
include=include, include=include,
exclude=exclude, exclude=exclude,
@ -443,6 +444,14 @@ def get_request_handler(
response_args["status_code"] = current_status_code response_args["status_code"] = current_status_code
if solved_result.response.status_code: if solved_result.response.status_code:
response_args["status_code"] = solved_result.response.status_code response_args["status_code"] = solved_result.response.status_code
# Use the fast path (dump_json) when no custom response
# class was set and a response field with a TypeAdapter
# exists. Serializes directly to JSON bytes via Pydantic's
# Rust core, skipping the intermediate Python dict +
# json.dumps() step.
use_dump_json = response_field is not None and isinstance(
response_class, DefaultPlaceholder
)
content = await serialize_response( content = await serialize_response(
field=response_field, field=response_field,
response_content=raw_response, response_content=raw_response,
@ -454,8 +463,16 @@ def get_request_handler(
exclude_none=response_model_exclude_none, exclude_none=response_model_exclude_none,
is_coroutine=is_coroutine, is_coroutine=is_coroutine,
endpoint_ctx=endpoint_ctx, endpoint_ctx=endpoint_ctx,
dump_json=use_dump_json,
) )
response = actual_response_class(content, **response_args) if use_dump_json:
response = Response(
content=content,
media_type="application/json",
**response_args,
)
else:
response = actual_response_class(content, **response_args)
if not is_body_allowed_for_status_code(response.status_code): if not is_body_allowed_for_status_code(response.status_code):
response.body = b"" response.body = b""
response.headers.raw.extend(solved_result.response.headers.raw) response.headers.raw.extend(solved_result.response.headers.raw)

51
tests/test_dump_json_fast_path.py

@ -0,0 +1,51 @@
from unittest.mock import patch
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.get("/default")
def get_default() -> Item:
return Item(name="widget", price=9.99)
@app.get("/explicit", response_class=JSONResponse)
def get_explicit() -> Item:
return Item(name="widget", price=9.99)
client = TestClient(app)
def test_default_response_class_skips_json_dumps():
"""When no response_class is set, the fast path serializes directly to
JSON bytes via Pydantic's dump_json and never calls json.dumps."""
with patch(
"starlette.responses.json.dumps", wraps=__import__("json").dumps
) as mock_dumps:
response = client.get("/default")
assert response.status_code == 200
assert response.json() == {"name": "widget", "price": 9.99}
mock_dumps.assert_not_called()
def test_explicit_response_class_uses_json_dumps():
"""When response_class is explicitly set to JSONResponse, the normal path
is used and json.dumps is called via JSONResponse.render()."""
with patch(
"starlette.responses.json.dumps", wraps=__import__("json").dumps
) as mock_dumps:
response = client.get("/explicit")
assert response.status_code == 200
assert response.json() == {"name": "widget", "price": 9.99}
mock_dumps.assert_called_once()
Loading…
Cancel
Save