{"record":{"id":"d645e36d1599d6a1","repo":"can1357/oh-my-pi","slug":"field-must-contain-string-keys","errorCode":null,"errorMessage":"{field} must contain string keys","messagePattern":"(.+?) must contain string keys","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/omp-rpc/src/omp_rpc/protocol.py","lineNumber":152,"sourceCode":"_ASSISTANT_ERROR_REASON_VALUES: Final[frozenset[str]] = frozenset({\"aborted\", \"error\"})\n_AUTO_COMPACTION_REASON_VALUES: Final[frozenset[str]] = frozenset(\n    {\"threshold\", \"overflow\", \"idle\", \"incomplete\"}\n)\n_AUTO_COMPACTION_ACTION_VALUES: Final[frozenset[str]] = frozenset(\n    {\"context-full\", \"handoff\", \"shake\", \"snapcompact\"}\n)\n\n\ndef _clone_json_value(value: object, *, field: str) -> JsonValue:\n    if value is None or isinstance(value, (str, int, float, bool)):\n        return cast(JsonValue, value)\n    if isinstance(value, list):\n        return [_clone_json_value(item, field=field) for item in value]\n    if isinstance(value, dict):\n        cloned: JsonObject = {}\n        for key, item in value.items():\n            if not isinstance(key, str):\n                raise ValueError(f\"{field} must contain string keys\")\n            cloned[key] = _clone_json_value(item, field=field)\n        return cloned\n    raise ValueError(f\"{field} must be JSON-serializable\")\n\n\ndef _clone_json_object(value: object, *, field: str) -> JsonObject:\n    if not isinstance(value, dict):\n        raise ValueError(f\"{field} must be an object\")\n    return cast(JsonObject, _clone_json_value(value, field=field))\n\n\ndef _optional_json_object(value: object, *, field: str) -> JsonObject | None:\n    if value is None:\n        return None\n    return _clone_json_object(value, field=field)\n\n\ndef _optional_json_objects(","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/omp-rpc/src/omp_rpc/protocol.py#L134-L170","documentation":"_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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\npayload = yaml.safe_load(cfg)          # {'8080': ..., 1: ...}\nnotif = parse_notification(payload)\n// after\npayload = {str(k): v for k, v in yaml.safe_load(cfg).items()}\nnotif = parse_notification(payload)","handlingStrategy":"type-guard","validationCode":"def stringify_keys(value):\n    if isinstance(value, dict):\n        return {str(k): stringify_keys(v) for k, v in value.items()}\n    if isinstance(value, list):\n        return [stringify_keys(v) for v in value]\n    return value","typeGuard":"def has_string_keys(value) -> bool:\n    if isinstance(value, dict):\n        return all(isinstance(k, str) and has_string_keys(v) for k, v in value.items())\n    if isinstance(value, list):\n        return all(has_string_keys(v) for v in value)\n    return True","tryCatchPattern":"try:\n    notif = parse_notification(message)\nexcept ValueError as e:\n    if 'must contain string keys' in str(e):\n        notif = parse_notification(stringify_keys(message))\n    else:\n        raise","preventionTips":["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"],"tags":["valueerror","json","protocol","key-type"],"backgroundTag":"non-string-json-key","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}