{"record":{"id":"3d37dc8e5f0baa4e","repo":"clockworklabs/SpacetimeDB","slug":"expected-a-json-object-at-the-top-level","errorCode":null,"errorMessage":"Expected a JSON object at the top level","messagePattern":"Expected a JSON object at the top level","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/bindings-typescript/src/server/runtime.ts","lineNumber":114,"sourceCode":"      headers: serializeHeaders(response.headers),\n      version: response.version,\n      code: response.status,\n    },\n    response.bytes(),\n  ];\n}\n\nexport function parseJsonObject(json: string): JsonObject {\n  let value: unknown;\n\n  try {\n    value = JSON.parse(json);\n  } catch {\n    throw new Error('Invalid JSON: failed to parse string');\n  }\n\n  if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n    throw new Error('Expected a JSON object at the top level');\n  }\n\n  // The runtime check above guarantees this cast is safe\n  return value as JsonObject;\n}\n\nclass JwtClaimsImpl implements JwtClaims {\n  readonly fullPayload: JsonObject;\n  private readonly _identity: Identity;\n  /**\n   * Creates a new JwtClaims instance.\n   * @param rawPayload The JWT payload as a raw JSON string.\n   * @param identity The identity for this JWT. We are only taking this because we don't have a blake3 implementation (which we need to compute it).\n   */\n  constructor(\n    public readonly rawPayload: string,\n    identity: Identity\n  ) {","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings-typescript/src/server/runtime.ts#L96-L132","documentation":"After a successful JSON.parse, parseJsonObject requires the top-level value to be a JSON object: not null, not an array, not a primitive. Callers such as JWT claims parsing (JwtClaimsImpl) index the result by claim name, so any other root type throws 'Expected a JSON object at the top level'.","triggerScenarios":"A request body that is a JSON array like '[1,2,3]' or a bare quoted string/number/boolean/null; a JWT whose payload decodes to an array or scalar instead of a claims object.","commonSituations":"Clients sending a top-level array where an object is expected; JSON.stringify of a non-object being stored and replayed; test fixtures with array roots.","solutions":["Send only {...} at the top level; wrap arrays as { \"items\": [...] }","Validate before calling: parse once, check typeof value === 'object' && value !== null && !Array.isArray(value)","Fix the token generator if JWT payloads are not claim objects"],"exampleFix":"// before\nconst data = parseJsonObject(text); // text = '[{\"id\":1}]' -> throws\n\n// after\nconst data = parseJsonObject(text); // text = '{\"items\":[{\"id\":1}]}'","handlingStrategy":"type-guard","validationCode":"function parseJsonObjectSafe(text: string): Record<string, unknown> | null {\n  let v: unknown;\n  try { v = JSON.parse(text); } catch { return null; }\n  return typeof v === 'object' && v !== null && !Array.isArray(v)\n    ? (v as Record<string, unknown>)\n    : null;\n}","typeGuard":"function isJsonObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":null,"preventionTips":["Fix API contracts to always use a top-level object; wrap arrays as { items: [...] }","Apply isJsonObject after any JSON.parse of external input before passing it onward","Keep JWT fixtures in tests as real claim objects, not arrays or scalars"],"tags":["json","parsing","jwt","typescript"],"backgroundTag":"invalid-json","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}