{"record":{"id":"980e7efc04bb84e9","repo":"commaai/openpilot","slug":"message-must-be-a-json-object","errorCode":null,"errorMessage":"message must be a JSON object","messagePattern":"message must be a JSON object","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"openpilot/system/athena/rpc.py","lineNumber":55,"sourceCode":"\ndef dumps_result(request_id: Any, result: Any) -> str:\n  return json.dumps({\"jsonrpc\": JSONRPC_VERSION, \"id\": request_id, \"result\": result})\n\n\ndef dumps_error(request_id: Any, message: str, code: int = SERVER_ERROR) -> str:\n  return json.dumps({\n    \"jsonrpc\": JSONRPC_VERSION,\n    \"id\": request_id,\n    \"error\": {\"code\": code, \"message\": message},\n  })\n\n\ndef loads(raw: str | bytes) -> JsonDict:\n  if isinstance(raw, bytes):\n    raw = raw.decode()\n  data = json.loads(raw)\n  if not isinstance(data, dict):\n    raise ValueError(\"message must be a JSON object\")\n  return data\n\n\ndef is_call(msg: JsonDict) -> bool:\n  return \"method\" in msg\n\n\ndef is_response(msg: JsonDict) -> bool:\n  return \"id\" in msg and (\"result\" in msg or \"error\" in msg)\n\n\ndef error_message(err: Any) -> str:\n  \"\"\"Normalize JSON-RPC object errors and plain-string errors.\"\"\"\n  if isinstance(err, str):\n    return err\n  if isinstance(err, dict):\n    data = err.get(\"data\")\n    if isinstance(data, dict) and data.get(\"message\"):","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/commaai/openpilot/blob/516ec1e68203439a73f340f1d0b3b91eabc626ee/openpilot/system/athena/rpc.py#L37-L73","documentation":"ValueError from openpilot/system/athena/rpc.py loads(): the payload is valid JSON but its root is not an object (dict). The JSON-RPC layer requires object-rooted messages; arrays, strings, numbers, booleans, or null roots are rejected before is_call/is_response dispatch.","triggerScenarios":"loads() receiving '[1,2,3]', '\"hello\"', '42', or 'null'. In athenad this is caught upstream and answered with a parse error; if you call loads()/handle() directly it raises.","commonSituations":"Clients sending a bare JSON array of batch requests (unsupported), a proxy or test harness double-encoding payloads (string root), or hand-typed websocket frames.","solutions":["Wrap the payload in a JSON-RPC 2.0 object envelope before sending","If integrating rpc.py directly, validate with isinstance(json.loads(raw), dict) before calling loads()","Batch requests are not supported: send one object per frame"],"exampleFix":"// before\nws.send(json.dumps([req1, req2]))\n\n// after\nfor req in (req1, req2):\n  ws.send(json.dumps(req))","handlingStrategy":"type-guard","validationCode":"import json\n\ndef is_json_object(raw) -> bool:\n    try:\n        return isinstance(json.loads(raw), dict)\n    except (ValueError, TypeError):\n        return False","typeGuard":"import json\nfrom typing import Any\n\ndef is_jsonrpc_envelope(data: Any) -> bool:\n    return isinstance(data, dict)","tryCatchPattern":"try:\n    msg = loads(raw)\nexcept ValueError as e:\n    if 'must be a JSON object' in str(e):\n        reply_parse_error(raw)  # notify sender of malformed envelope\n    raise","preventionTips":["Never send JSON arrays or scalars as websocket message roots","Encode requests with a shared helper that always emits an object envelope"],"tags":["jsonrpc","json","athena","validation"],"backgroundTag":null,"analyzedSha":"516ec1e68203439a73f340f1d0b3b91eabc626ee","analyzedAt":"2026-08-15T00:17:37.461Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}