Skip to content

pydantic_jsonpointer.adapters

pydantic_jsonpointer.adapters

Container adapter Protocol and registry.

A ContainerAdapter teaches the traversal layer how to read/write a specific container type. The registry maps runtime types to adapter instances, with MRO-aware lookup so subclasses inherit registration.

unwrap() uses the built-in Ellipsis singleton (...) as its "no unwrap" return marker. Adapters MUST return ... to mean "use the value as-is"; returning a real Ellipsis payload (e.g. a RootModel wrapping Ellipsis) would be indistinguishable from "no unwrap". This is a documented constraint on adapter authors; the practical impact is nil because no real document contains ....

ContainerAdapter

Bases: Protocol

Per-type strategy for reading/writing a container at a JSON-pointer key.

Every public method receives parent (the container) and (for non-resolve methods) key (already coerced via resolve_token). Adapters are expected to be stateless — they are shared across calls.

unwrap returns ... (Ellipsis) to mean "no unwrap; use value as-is", or the inner value to substitute transparently.

Adapters may OPTIONALLY define is_value_frozen(value) -> bool: when present, the walker treats it as an additional taint source so that an immutable wrapper (e.g. a frozen RootModel) propagates the freeze bit through unwrap to its inner payload. Adapters that omit the hook — including any third-party adapter written against the canonical 8-method contract — are treated as if it returned False. The hook is deliberately outside the required Protocol surface so legacy adapters keep satisfying isinstance(adapter, ContainerAdapter) and static type-checks against register().

DictAdapter

Adapter for built-in dict containers.

Tokens are used verbatim as keys (already RFC-6901-unescaped by JsonPointer).

ListAdapter

Adapter for built-in list containers.

RFC 6901 array-index rules: the token must be either "-" (tail marker, only valid for add) or a run of decimal digits with no leading zero (except the single-character "0").

TupleAdapter

Adapter for built-in tuple containers (read-only).

Tuples are immutable: set, add, and remove all raise ImmutableTargetError. is_step_frozen returns False so that mutable objects inside a tuple slot remain writable via their own adapters.

adapter_for

adapter_for(
    value: Any, *, resolver: Any = None
) -> ContainerAdapter

Look up the adapter for type(value) via MRO walk.

If resolver is provided AND the resolved adapter is a BaseModelAdapter (or subclass), returns a per-call clone produced by adapter.with_resolver(resolver) — a shallow copy of the registered instance with its resolver substituted. Subclass identity and any extra state on the registered instance are preserved; subclasses with state that needs custom cloning can override with_resolver. The registered adapter itself is never mutated.

Raises AdapterNotFoundError if no adapter is registered for any class in the value's MRO.

Source code in src/pydantic_jsonpointer/adapters.py
def adapter_for(value: Any, *, resolver: Any = None) -> ContainerAdapter:
    """Look up the adapter for `type(value)` via MRO walk.

    If `resolver` is provided AND the resolved adapter is a
    ``BaseModelAdapter`` (or subclass), returns a per-call clone produced by
    ``adapter.with_resolver(resolver)`` — a shallow copy of the registered
    instance with its resolver substituted. Subclass identity and any extra
    state on the registered instance are preserved; subclasses with state
    that needs custom cloning can override ``with_resolver``. The registered
    adapter itself is never mutated.

    Raises AdapterNotFoundError if no adapter is registered for any class
    in the value's MRO.
    """
    for cls in type(value).__mro__:
        adapter = _REGISTRY.get(cls)
        if adapter is not None:
            if resolver is not None:
                from .pydantic_adapter import BaseModelAdapter

                if isinstance(adapter, BaseModelAdapter):
                    return adapter.with_resolver(resolver)
            return adapter
    raise AdapterNotFoundError(
        f"no adapter registered for type {type(value).__name__!r}"
    )

register

register(
    container_type: type,
    adapter: ContainerAdapter,
    *,
    override: bool = False,
) -> None

Register adapter as the handler for container_type.

Subsequent adapter_for(value) calls return adapter when type(value) is container_type or a subclass of it (via MRO walk).

Raises ValueError if the type is already registered unless override=True.

Source code in src/pydantic_jsonpointer/adapters.py
def register(
    container_type: type,
    adapter: ContainerAdapter,
    *,
    override: bool = False,
) -> None:
    """Register `adapter` as the handler for `container_type`.

    Subsequent `adapter_for(value)` calls return `adapter` when
    `type(value)` is `container_type` or a subclass of it (via MRO walk).

    Raises ValueError if the type is already registered unless override=True.
    """
    if not override and container_type in _REGISTRY:
        raise ValueError(
            f"adapter already registered for {container_type!r}; "
            f"pass override=True to replace it"
        )
    _REGISTRY[container_type] = adapter