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
- Convert non-JSON values to JSON-compatible ones before signing: Date -> toISOString(), bigint -> string/number, Map/Set -> plain objects/arrays.
- Strip functions, symbols, and undefined array entries from the continuation payload.
- JSON round-trip the payload (with a safe parse utility) before calling signCodeModeContinuation to normalize it.
- Keep the continuation payload limited to plain serializable data (strings, numbers, booleans, nulls, arrays, plain objects).
- 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
- Keep continuation payloads limited to plain JSON types: string, number, boolean, null, array, plain object.
- Convert Date objects with toISOString() and bigints with String() before signing.
- Strip functions, symbols, and undefined array entries from payloads.
- JSON round-trip dynamic provider data before embedding it in a continuation.
- Add a pre-sign serializability assertion in your own wrapper around signCodeModeContinuation.
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
- Code mode interrupt payload must be an object.
- Code mode interrupt payload must include a string kind.
- CODE_MODE_PROTOCOL_ERROR
- CODE_MODE_PROTOCOL_ERROR
- CODE_MODE_PROTOCOL_ERROR
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/13411b5a19e71dc0.
Report an issue: GitHub.