vercel/ai · error · TypeError

Continuation contains a non-JSON-serializable value.

Error message

Continuation contains a non-JSON-serializable value.

What it means

canonicalJson builds a deterministic, key-sorted JSON serialization of the continuation so the HMAC signature is stable. It only supports null, string, number, boolean, arrays, and plain objects (undefined object values are skipped). Any other value — functions, symbols, bigint, Date objects, undefined at array/top level, Map/Set, class instances' non-enumerable bits — makes deterministic signing impossible and triggers this TypeError. It is thrown from canonicalJson, called by signContinuationPayload (and recursively by canonicalJson itself).

Source

Thrown at packages/code-mode/src/continuation-capability.ts:218

  }
  if (typeof value === 'string') {
    return JSON.stringify(value);
  }
  if (typeof value === 'number' || typeof value === 'boolean') {
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    return `[${value.map(canonicalJson).join(',')}]`;
  }
  if (typeof value === 'object') {
    const entries = Object.entries(value as Record<string, unknown>)
      .filter(([, item]) => item !== undefined)
      .sort(([left], [right]) => left.localeCompare(right));
    return `{${entries
      .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
      .join(',')}}`;
  }
  throw new TypeError('Continuation contains a non-JSON-serializable value.');
}

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Convert non-JSON values to JSON-compatible ones before signing: Date -> toISOString(), bigint -> string/number, Map/Set -> plain objects/arrays.
  2. Strip functions, symbols, and undefined array entries from the continuation payload.
  3. JSON round-trip the payload (with a safe parse utility) before calling signCodeModeContinuation to normalize it.
  4. Keep the continuation payload limited to plain serializable data (strings, numbers, booleans, nulls, arrays, plain objects).
  5. Wrap signCodeModeContinuation calls in a check that deep-validates serializability when payload content comes from dynamic sources.

Example fix

// before
const continuation = signCodeModeContinuation({
  ...base,
  pendingInterruptions: [{ toolCallId: 't1', requestedAt: new Date() }],
});
// after
const continuation = signCodeModeContinuation({
  ...base,
  pendingInterruptions: [{ toolCallId: 't1', requestedAt: new Date().toISOString() }],
});
Defensive patterns

Strategy: validation

Validate before calling

function isJsonSerializable(value: unknown, seen = new Set()): boolean {
  if (value === null || ['string','number','boolean'].includes(typeof value)) return true;
  if (typeof value === 'bigint' || typeof value === 'function' || typeof value === 'symbol') return false;
  if (value instanceof Date || value instanceof Map || value instanceof Set || value instanceof RegExp) return false;
  if (seen.has(value)) return true;
  seen.add(value);
  if (Array.isArray(value)) return value.every(v => v !== undefined && isJsonSerializable(v, seen));
  if (typeof value === 'object') return Object.values(value).every(v => isJsonSerializable(v, seen));
  return false;
}
if (!isJsonSerializable(unsignedContinuation)) {
  throw new Error('Normalize the continuation payload to plain JSON before signing');
}

Type guard

function isPlainJsonValue(value: unknown): boolean {
  return value === null || ['string','number','boolean'].includes(typeof value) ||
    (Array.isArray(value) && value.every(isPlainJsonValue)) ||
    (typeof value === 'object' && value !== null && !(value instanceof Date) && Object.values(value).every(isPlainJsonValue));
}

Try / catch

try {
  const signed = signCodeModeContinuation(unsignedContinuation);
} catch (e) {
  if (e instanceof TypeError && /non-JSON-serializable/.test(e.message)) {
    const normalized = JSON.parse(JSON.stringify(unsignedContinuation, (_k, v) =>
      typeof v === 'bigint' ? v.toString() : v instanceof Date ? v.toISOString() : v));
    const signed = signCodeModeContinuation(normalized);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling signCodeModeContinuation (or verifyCodeModeContinuation, which re-serializes via signContinuationPayload) with a continuation containing a value canonicalJson cannot represent: a Date, bigint, function, symbol, Map/Set, RegExp, or an array element of undefined.

Common situations: Embedding provider tool results that include Date objects into pendingInterruptions/resolutions; adding a bigint counter or function reference to the continuation payload; passing a class instance with inherited fields; forgetting to JSON-normalize values fetched from another API before signing.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/13411b5a19e71dc0. Report an issue: GitHub.