Source code for next.static.serializers

"""Pluggable JS-context serializers for `@context(serialize=True)` values.

`StaticCollector.add_js_context` delegates value encoding to a
`JsContextSerializer`. The default implementation uses
`DjangoJSONEncoder`, which handles the same set of types that the
framework has always accepted. Applications that want to serialise
pydantic models, msgspec structs, or any other type can point the
`JS_CONTEXT_SERIALIZER` option at a class that implements the
protocol.
"""

from __future__ import annotations

import json
from typing import Any, Protocol, runtime_checkable

from django.core.serializers.json import DjangoJSONEncoder

from next.conf import import_class_cached, next_framework_settings


[docs] @runtime_checkable class JsContextSerializer(Protocol): """Encode values destined for `window.Next.context`. Implementations turn Python values into JSON text. The contract is deliberately narrow so that custom types can travel to the client without bolt-on Django encoder extensions. """
[docs] def dumps(self, value: Any) -> str: # noqa: ANN401 """Return a JSON string for `value`.""" raise NotImplementedError
[docs] class JsonJsContextSerializer: """Serialise values with Django's `DjangoJSONEncoder`. This is the process-wide default. It mirrors the behaviour built into the collector before serializers became pluggable. The output uses compact separators so the inline init payload stays small. """
[docs] def dumps(self, value: Any) -> str: # noqa: ANN401 """Return a compact JSON string produced by `json.dumps`.""" return json.dumps(value, cls=DjangoJSONEncoder, separators=(",", ":"))
[docs] class PydanticJsContextSerializer: """Serialise values through pydantic model dump when available. Unknown types fall through to `DjangoJSONEncoder`, so lists and dicts containing mixed pydantic and plain values still serialise without a second code path. """
[docs] def __init__(self) -> None: """Import pydantic lazily so tests without it keep working.""" try: import pydantic # noqa: PLC0415 except ImportError as e: msg = ( "PydanticJsContextSerializer requires the pydantic package. " "Install it or switch JS_CONTEXT_SERIALIZER to another class." ) raise ImportError(msg) from e self._pydantic = pydantic
[docs] def dumps(self, value: Any) -> str: # noqa: ANN401 """Return a compact JSON string with pydantic models unwrapped.""" return json.dumps(value, cls=_PydanticAwareEncoder, separators=(",", ":"))
class _PydanticAwareEncoder(DjangoJSONEncoder): """Fallback encoder that unwraps pydantic `BaseModel` instances.""" def default(self, o: Any) -> Any: # noqa: ANN401 """Dump `BaseModel` subclasses via `model_dump` before deferring.""" import pydantic # noqa: PLC0415 if isinstance(o, pydantic.BaseModel): return o.model_dump(mode="json") return super().default(o) _default_serializer: JsContextSerializer = JsonJsContextSerializer()
[docs] def resolve_serializer() -> JsContextSerializer: """Return the configured serializer or the process-wide default. The resolver reads `NEXT_FRAMEWORK["JS_CONTEXT_SERIALIZER"]` on every call. Returning a fresh instance each time keeps the hot path free of caching edge cases during test overrides. """ path = getattr(next_framework_settings, "JS_CONTEXT_SERIALIZER", None) if not path: return _default_serializer cls = import_class_cached(str(path)) instance = cls() if not isinstance(instance, JsContextSerializer): msg = f"{path!r} does not implement JsContextSerializer" raise TypeError(msg) return instance
__all__ = [ "JsContextSerializer", "JsonJsContextSerializer", "PydanticJsContextSerializer", "resolve_serializer", ]