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

  1. Send only {...} at the top level; wrap arrays as { "items": [...] }
  2. Validate before calling: parse once, check typeof value === 'object' && value !== null && !Array.isArray(value)
  3. 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

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


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/3d37dc8e5f0baa4e. Report an issue: GitHub.