# JSON encoding EDRI supports custom JSON encoding for REST and WebSocket payloads. This document describes how to implement a custom encoder, how encoder context works, and how to serialize complex types. --- ## Where the encoder is used A custom encoder is used when EDRI renders JSON for: * REST responses (`application/json`) * WebSocket messages (JSON payloads) --- ## Configuring a custom encoder Pass your encoder class when starting the API: ```python edri.start_api( middlewares=[...], json_encoder=MyJSONEncoder, ) ``` --- ## Base class: `CustomJSONEncoder` Custom encoders should inherit from: * `edri.utility.json_encoder.CustomJSONEncoder` Override `default(...)` to add support for custom types. --- ## Example: serializing IP addresses ```python from ipaddress import IPv4Address, IPv6Address from typing import Any from edri.utility.json_encoder import CustomJSONEncoder class MyJSONEncoder(CustomJSONEncoder): def default(self, data: Any) -> Any: if isinstance(data, (IPv4Address, IPv6Address)): return data.exploded return super().default(data) ``` --- ## Encoder context Encoders may use `self.context` to access request-scoped information. In EDRI, the encoder context is derived from the event itself. * The API layer calls `event.get_context()`. * The result is passed to the JSON encoder as `self.context`. ### What `Event.get_context()` returns `Event.get_context()` returns a dictionary of all **keyword-only** dataclass fields (`kw_only=True`) and their current values. This makes keyword-only fields the recommended place for request-scoped inputs that should be available to JSON encoding (for example values loaded from headers/cookies/scope, or other per-request metadata). Typical uses: * attaching a database session wrapper * controlling output detail level (public vs internal) * passing authenticated user/session info into model serializers `context` is intended to be read by the encoder. --- (example-serializing-orm-models-with-a-session)= ## Example: serializing ORM models with a session This pattern: * detects objects that implement `to_json(...)` * merges them into a SQLAlchemy session * returns a JSON-friendly structure ```python from typing import Any from edri.utility.json_encoder import CustomJSONEncoder from config.database import SessionFactory from database.models import Base class SessionJSONEncoder(CustomJSONEncoder): def default(self, data: Any) -> Any: if hasattr(data, "to_json") and issubclass(type(data), Base): # Reuse a request-scoped session passed via context if present, # otherwise open a short-lived one. session = (self.context or {}).get("session") if session is not None: return session.merge(data, load=True).to_json(session) with SessionFactory() as session: return session.merge(data, load=True).to_json(session) return super().default(data) ``` > A ready-made, base-class-agnostic version ships as an **opt-in integration**: > `from edri.integrations.sqlalchemy import SessionJSONEncoder`. Subclass it and > implement `get_session()` to return a context-managed `Session`, then pass the > subclass to `edri.start_api(json_encoder=...)`. It is not a core dependency — > install it with `pip install edri[sqlalchemy]`. See > {ref}`optional-integrations`. --- ## Gotchas * Always call `super().default(...)` for types you do not handle. * Keep encoders fast; they run in the response rendering path. * Avoid doing heavy database work per field if you can batch/prepare data earlier. --- ## Related * {doc}`/api` – API setup, Accept header, middleware * {doc}`/response` – response model and directives