clockworklabs/SpacetimeDB · error · Error
Expected a JSON object at the top level
Error message
Expected a JSON object at the top level
What it means
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'.
Source
Thrown at crates/bindings-typescript/src/server/runtime.ts:114
headers: serializeHeaders(response.headers),
version: response.version,
code: response.status,
},
response.bytes(),
];
}
export function parseJsonObject(json: string): JsonObject {
let value: unknown;
try {
value = JSON.parse(json);
} catch {
throw new Error('Invalid JSON: failed to parse string');
}
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Expected a JSON object at the top level');
}
// The runtime check above guarantees this cast is safe
return value as JsonObject;
}
class JwtClaimsImpl implements JwtClaims {
readonly fullPayload: JsonObject;
private readonly _identity: Identity;
/**
* Creates a new JwtClaims instance.
* @param rawPayload The JWT payload as a raw JSON string.
* @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).
*/
constructor(
public readonly rawPayload: string,
identity: Identity
) {View on GitHub (pinned to 524b4487d9)
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
Example fix
// before
const data = parseJsonObject(text); // text = '[{"id":1}]' -> throws
// after
const data = parseJsonObject(text); // text = '{"items":[{"id":1}]}' Defensive patterns
Strategy: type-guard
Validate before calling
function parseJsonObjectSafe(text: string): Record<string, unknown> | null {
let v: unknown;
try { v = JSON.parse(text); } catch { return null; }
return typeof v === 'object' && v !== null && !Array.isArray(v)
? (v as Record<string, unknown>)
: null;
} Type guard
function isJsonObject(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid JSON: failed to parse string
- Cannot convert ${typeof value} to ${what}: expected bigint,
- Invalid hex UUID
- cannot serialize refs without a typespace
- cannot deserialize refs without a typespace
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/3d37dc8e5f0baa4e.
Report an issue: GitHub.