can1357/oh-my-pi · error · ValueError

{field} must be JSON-serializable

Error message

{field} must be JSON-serializable

What it means

omp-rpc validates incoming RPC payloads by deep-cloning values into strict JSON types before constructing protocol dataclasses. `_clone_json_value` accepts only None, bool, int, float, str, list, and dict; anything else (e.g. datetime, bytes, Decimal, set, custom objects, or numpy scalars) raises this ValueError naming the offending field. It exists because the wire contract is pure JSON, and silently stringifying non-JSON values would hide client-side serialization bugs.

Source

Thrown at python/omp-rpc/src/omp_rpc/protocol.py:155

)
_AUTO_COMPACTION_ACTION_VALUES: Final[frozenset[str]] = frozenset(
    {"context-full", "handoff", "shake", "snapcompact"}
)


def _clone_json_value(value: object, *, field: str) -> JsonValue:
    if value is None or isinstance(value, (str, int, float, bool)):
        return cast(JsonValue, value)
    if isinstance(value, list):
        return [_clone_json_value(item, field=field) for item in value]
    if isinstance(value, dict):
        cloned: JsonObject = {}
        for key, item in value.items():
            if not isinstance(key, str):
                raise ValueError(f"{field} must contain string keys")
            cloned[key] = _clone_json_value(item, field=field)
        return cloned
    raise ValueError(f"{field} must be JSON-serializable")


def _clone_json_object(value: object, *, field: str) -> JsonObject:
    if not isinstance(value, dict):
        raise ValueError(f"{field} must be an object")
    return cast(JsonObject, _clone_json_value(value, field=field))


def _optional_json_object(value: object, *, field: str) -> JsonObject | None:
    if value is None:
        return None
    return _clone_json_object(value, field=field)


def _optional_json_objects(
    values: object, *, field: str
) -> tuple[JsonObject, ...] | None:
    if values is None:

View on GitHub (pinned to 9690622007)

Solutions

  1. Locate the field named in the message and convert non-JSON values to JSON equivalents before calling the parser (datetime -> .isoformat(), UUID -> str(), Decimal -> float, set -> sorted list).
  2. Run the payload through `json.loads(json.dumps(payload, default=str))` as a normalizer when you cannot control every nested value.
  3. Use only values produced by `json.loads` / a JSON parser to build RPC payloads instead of hand-constructing dicts from Python objects.
  4. Enable a strict encoder (e.g. json.dumps with no `default`) in tests to fail fast on non-serializable values.

Example fix

// before
from datetime import datetime
parse_tool_descriptor({"name": "run", "metadata": {"created": datetime.now()}})
// ValueError: metadata must be JSON-serializable

// after
parse_tool_descriptor({"name": "run", "metadata": {"created": datetime.now().isoformat()}})
Defensive patterns

Strategy: validation

Validate before calling

import json
def is_json_serializable(value) -> bool:
    try:
        json.dumps(value)
        return True
    except (TypeError, ValueError):
        return False

assert is_json_serializable(payload), f"{field} payload is not JSON-serializable"

Type guard

JSON_PRIMITIVES = (str, int, float, bool, type(None))
def is_json_value(value) -> bool:
    if isinstance(value, JSON_PRIMITIVES):
        return True
    if isinstance(value, list):
        return all(is_json_value(item) for item in value)
    if isinstance(value, dict):
        return all(isinstance(k, str) and is_json_value(v) for k, v in value.items())
    return False

Prevention

When it happens

Trigger: Calling parse functions like parse_tool_descriptor, parse_compaction_result, parse_notification, or the internal _clone_json_object with a dict that contains a non-JSON-serializable value anywhere in its nesting (e.g. a `datetime` in a tool `metadata` field, `bytes` in a payload, a `set`, or a Decimal for a float field).

Common situations: Passing Python SDK objects (datetime, UUID, Decimal) directly into RPC payloads instead of pre-converting to ISO strings; building arguments with objects from another library (numpy types, dataclass instances) without `json.dumps`-style conversion; constructing payloads by hand rather than via `json.loads` of a JSON document.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/43353b99a4ca7fba. Report an issue: GitHub.