can1357/oh-my-pi · error · ValueError
{field} must contain string keys
Error message
{field} must contain string keys What it means
_clone_json_value() deep-copies a value while asserting it is valid JSON for the RPC protocol. JSON object keys must be strings; if a dict with a non-str key (int, bool, None, tuple) reaches a parse/copy boundary such as parse_tool_descriptor, parse_compaction_result, or parse_notification, ValueError is raised naming the offending field. This guards against silently coercing or dropping keys when crossing the protocol boundary.
Source
Thrown at python/omp-rpc/src/omp_rpc/protocol.py:152
_ASSISTANT_ERROR_REASON_VALUES: Final[frozenset[str]] = frozenset({"aborted", "error"})
_AUTO_COMPACTION_REASON_VALUES: Final[frozenset[str]] = frozenset(
{"threshold", "overflow", "idle", "incomplete"}
)
_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(View on GitHub (pinned to 9690622007)
Solutions
- Convert keys before parsing: `{str(k): v for k, v in data.items()}` (recursively for nested dicts)
- If coming from YAML, load with a wrapper that stringifies keys or avoid unquoted numeric keys in the YAML file
- Locate which 'field' the message names in the error and fix that specific structure at its source
Example fix
// before
payload = yaml.safe_load(cfg) # {'8080': ..., 1: ...}
notif = parse_notification(payload)
// after
payload = {str(k): v for k, v in yaml.safe_load(cfg).items()}
notif = parse_notification(payload) Defensive patterns
Strategy: type-guard
Validate before calling
def stringify_keys(value):
if isinstance(value, dict):
return {str(k): stringify_keys(v) for k, v in value.items()}
if isinstance(value, list):
return [stringify_keys(v) for v in value]
return value Type guard
def has_string_keys(value) -> bool:
if isinstance(value, dict):
return all(isinstance(k, str) and has_string_keys(v) for k, v in value.items())
if isinstance(value, list):
return all(has_string_keys(v) for v in value)
return True Try / catch
try:
notif = parse_notification(message)
except ValueError as e:
if 'must contain string keys' in str(e):
notif = parse_notification(stringify_keys(message))
else:
raise Prevention
- Run a recursive key-stringify pass on data from YAML/msgpack/db sources before protocol parsing
- Quote numeric keys in YAML (e.g. `'8080':`) so parsers keep them as strings
- Avoid dict/tuple/enum keys in structures destined for JSON protocols
- Validate JSON-serializability at system boundaries with a pre-flight walk before calling parse_* functions
When it happens
Trigger: Passing a Python dict parsed from non-JSON sources with int keys (e.g. {1: 'a'}) into descriptor/notification structures; constructing payloads with enum or tuple keys; deserializing from formats that allow non-string keys (YAML, msgpack, Python pickle) and feeding the result to the protocol parser.
Common situations: YAML configs parsed with yaml.safe_load (which keeps int keys like `8080:`), data from databases/msgpack with integer keys, or a message built programmatically where a constant was an enum.
Related errors
- Replacement text is not valid UTF-8: {err}
- rpc frame must be an object
- Invalid cmux socket JSON response: ${err instanceof Error ?
- Invalid cmux socket response: expected object
- Invalid cmux socket response: missing ok flag
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/d645e36d1599d6a1.
Report an issue: GitHub.