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
- 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).
- Run the payload through `json.loads(json.dumps(payload, default=str))` as a normalizer when you cannot control every nested value.
- Use only values produced by `json.loads` / a JSON parser to build RPC payloads instead of hand-constructing dicts from Python objects.
- 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
- Build RPC payloads exclusively from json.loads output or JSON literals, never from live Python objects.
- Convert datetime/UUID/Decimal/set values to strings or lists at payload-construction time.
- Add a json.dumps round-trip assertion in tests for any payload you construct.
- Remember json.dumps' default=str only masks the problem — fix the value itself.
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
- rpc frame must be an object
- RPC frame must be a JSON object
- {field} must be an object
- {field} must be a list
- {field} must be one of: {expected}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/43353b99a4ca7fba.
Report an issue: GitHub.