Skip to content

Extending

Customization hooks for filter operators and value serialization.

Custom filter operators

register_op

register_op(
    name: str,
    *,
    wire: str | None = None,
    builder: BuilderCall | None = None,
    predicate: ValueFormatter | None = None,
) -> Callable[[BuilderCall], BuilderCall] | BuilderCall

Register a filter operator (decorator or callable).

Used as a decorator::

@register_op("starts_with")
def starts_with(b, col, val): return b.like(col, f"{val}%")

Or directly::

register_op("starts_with", builder=starts_with)

Parameters:

Name Type Description Default
name str

Python method name exposed as Model.query.<name>(col, val).

required
wire str | None

PostgREST operator string for predicate groups (col.OP.val). Defaults to name. Override when name collides with a Python keyword — e.g. register_op("in_", wire="in").

None
builder BuilderCall | None

The implementation function. Omit when using as a decorator.

None
predicate ValueFormatter | None

Value-only formatter (val) -> str for predicate strings. Defaults to :func:_pred_value (handles scalars, sequences, comma-quoting). Override for ops with a non-standard value shape.

None

Returns:

Type Description
Callable[[BuilderCall], BuilderCall] | BuilderCall

The registered function when builder is given, otherwise a decorator.

Source code in src/supabase_orm/_filters.py
def register_op(
    name: str,
    *,
    wire: str | None = None,
    builder: BuilderCall | None = None,
    predicate: ValueFormatter | None = None,
) -> Callable[[BuilderCall], BuilderCall] | BuilderCall:
    """Register a filter operator (decorator or callable).

    Used as a decorator::

        @register_op("starts_with")
        def starts_with(b, col, val): return b.like(col, f"{val}%")

    Or directly::

        register_op("starts_with", builder=starts_with)

    Args:
        name: Python method name exposed as ``Model.query.<name>(col, val)``.
        wire: PostgREST operator string for predicate groups (``col.OP.val``).
            Defaults to ``name``. Override when ``name`` collides with a Python
            keyword — e.g. ``register_op("in_", wire="in")``.
        builder: The implementation function. Omit when using as a decorator.
        predicate: Value-only formatter ``(val) -> str`` for predicate strings.
            Defaults to :func:`_pred_value` (handles scalars, sequences,
            comma-quoting). Override for ops with a non-standard value shape.

    Returns:
        The registered function when ``builder`` is given, otherwise a decorator.
    """
    wire_name = wire or name

    def _do(fn: BuilderCall) -> BuilderCall:
        OPERATORS[name] = fn
        WIRE_NAMES[name] = wire_name
        VALUE_FORMATTERS[name] = predicate or _pred_value
        return fn

    if builder is not None:
        _do(builder)
        return builder
    return _do

Custom serializers

register_serializer

register_serializer(tp: type, fn: Serializer) -> None

Register a wire-value serializer for tp and its subclasses.

The serializer fn is called by :func:serialize whenever it encounters a value of type tp (or a subclass). Registration is process-global; the resolved-subclass cache is cleared so previously cached dispatch decisions get re-evaluated against the new entry.

Example
from supabase_orm import register_serializer

class Money:
    def __init__(self, cents: int) -> None:
        self.cents = cents

register_serializer(Money, lambda v: v.cents)

Parameters:

Name Type Description Default
tp type

The exact Python type to register. Subclasses dispatch via isinstance walk on first encounter (then cached).

required
fn Serializer

Callable that takes a value and returns a JSON-native form (str / int / float / bool / None / list / dict).

required
Source code in src/supabase_orm/_serializers.py
def register_serializer(tp: type, fn: Serializer) -> None:
    """Register a wire-value serializer for ``tp`` and its subclasses.

    The serializer ``fn`` is called by :func:`serialize` whenever it
    encounters a value of type ``tp`` (or a subclass). Registration is
    process-global; the resolved-subclass cache is cleared so previously
    cached dispatch decisions get re-evaluated against the new entry.

    Example:
        ```python
        from supabase_orm import register_serializer

        class Money:
            def __init__(self, cents: int) -> None:
                self.cents = cents

        register_serializer(Money, lambda v: v.cents)
        ```

    Args:
        tp: The exact Python type to register. Subclasses dispatch via
            ``isinstance`` walk on first encounter (then cached).
        fn: Callable that takes a value and returns a JSON-native form
            (``str`` / ``int`` / ``float`` / ``bool`` / ``None`` / ``list`` /
            ``dict``).
    """
    _REGISTRY[tp] = fn
    _RESOLVED.clear()

serialize

serialize(value: Any) -> Any

Coerce value to a JSON-native form using the registered serializers.

Dispatch order:

  1. str / int / float / bool / None — returned as-is.
  2. Exact-type or resolved-subclass cache hit — registered serializer fires.
  3. First-time subclass — walk the registry, cache the match.
  4. dict / list / tuple / set — recurse element-by-element.
  5. Unknown — returned unchanged (postgrest-py decides).

Built-in registrations: UUID → str, datetime / date → ISO 8601 strings, Decimal → str, Enum.value, Pydantic BaseModelmodel_dump(mode="json", **__model_dump_kwargs__).

Parameters:

Name Type Description Default
value Any

Any Python value the ORM is about to send on the wire.

required

Returns:

Type Description
Any

A JSON-serializable form of value.

Source code in src/supabase_orm/_serializers.py
def serialize(value: Any) -> Any:
    """Coerce ``value`` to a JSON-native form using the registered serializers.

    Dispatch order:

    1. ``str`` / ``int`` / ``float`` / ``bool`` / ``None`` — returned as-is.
    2. Exact-type or resolved-subclass cache hit — registered serializer fires.
    3. First-time subclass — walk the registry, cache the match.
    4. ``dict`` / ``list`` / ``tuple`` / ``set`` — recurse element-by-element.
    5. Unknown — returned unchanged (postgrest-py decides).

    Built-in registrations: ``UUID`` → str, ``datetime`` / ``date`` →
    ISO 8601 strings, ``Decimal`` → str, ``Enum`` → ``.value``,
    Pydantic ``BaseModel`` → ``model_dump(mode="json", **__model_dump_kwargs__)``.

    Args:
        value: Any Python value the ORM is about to send on the wire.

    Returns:
        A JSON-serializable form of ``value``.
    """
    tp = type(value)
    # Fast-path: JSON-native scalars are by far the most common values
    # flowing through filter operators and write payloads.
    if tp in _JSON_NATIVE:
        return value
    # Exact-type and resolved-subclass caches — both O(1) dict lookups.
    fn = _REGISTRY.get(tp) or _RESOLVED.get(tp)
    if fn is not None:
        return fn(value)
    # First time we see this type: walk the registry once, cache the result.
    for base, fn in _REGISTRY.items():
        if isinstance(value, base):
            _RESOLVED[tp] = fn
            return fn(value)
    if isinstance(value, dict):
        return {k: serialize(v) for k, v in value.items()}
    if isinstance(value, (list, tuple, set)):
        return [serialize(v) for v in value]
    return value