Source code for loom.rest.fastapi.response
"""MsgspecJSONResponse — zero-copy JSON response for FastAPI.
Uses ``msgspec.json.encode`` to serialise response content directly to bytes,
bypassing the stdlib ``json`` module and Pydantic's serialisation pipeline.
No double-serialisation occurs: the bytes are written to the response body
exactly once.
Supports any type that ``msgspec.json.encode`` accepts: plain Python types
(``dict``, ``list``, ``str``, ``int``, etc.), ``msgspec.Struct`` instances,
and ``dataclasses``. A type msgspec declines — a strict ``pydantic.BaseModel``
result, a ``list`` of them, or an envelope holding one — falls through to
``enc_hook``, which renders it through the loom type instead.
Usage::
from loom.rest.fastapi.response import MsgspecJSONResponse
@app.get("/items/{item_id}")
async def get_item(item_id: int) -> MsgspecJSONResponse:
item = await repo.get(item_id)
return MsgspecJSONResponse(content=item)
"""
from __future__ import annotations
from typing import Any
import msgspec
from starlette.responses import Response
from loom.core.model import UnsupportedBoundaryType, loom_type_of
[docs]
class MsgspecJSONResponse(Response):
"""FastAPI ``Response`` subclass that encodes content with ``msgspec.json``.
Drop-in replacement for ``fastapi.responses.JSONResponse`` with two
advantages:
- Native ``msgspec.Struct`` serialisation — no ``dict`` conversion needed.
- Single encoding pass — content is written directly to bytes with no
intermediate JSON string or Pydantic round-trip.
Args:
content: Any object supported by ``msgspec.json.encode``.
status_code: HTTP status code. Defaults to ``200``.
headers: Additional response headers.
media_type: Defaults to ``"application/json"``.
background: Optional Starlette background task.
Example::
return MsgspecJSONResponse(content=my_struct, status_code=201)
"""
media_type = "application/json"
[docs]
def render(self, content: object) -> bytes:
"""Encode ``content`` to JSON bytes using ``msgspec.json.encode``.
Args:
content: Object to serialise.
Returns:
UTF-8 encoded JSON bytes.
Raises:
TypeError: ``content``, or a value nested inside it, is a type
neither msgspec nor pydantic can render.
pydantic_core.PydanticSerializationError: A nested
``pydantic.BaseModel`` fails its own serialisation; pydantic
raises it directly and it propagates unchanged.
"""
return msgspec.json.encode(content, enc_hook=_to_builtins)
def _to_builtins(obj: Any) -> Any:
"""Render a type ``msgspec.json.encode`` cannot handle on its own.
Runs only for a type msgspec declines — a strict ``pydantic.BaseModel``
nested inside the response, most often — so a Struct, a builtin or
``None`` keeps paying for a single encode pass.
Raises:
TypeError: ``obj``'s type is not a boundary type either library
knows how to render.
"""
try:
return loom_type_of(type(obj)).to_builtins(obj)
except UnsupportedBoundaryType as exc:
raise TypeError(str(exc)) from exc