JSON.parse's reviver callback now gets a third argument — a context object exposing the original source text of each value. Pair that with JSON.rawJSON() for the round trip. Both shipped as Baseline 2025, which means every evergreen browser and Node 22+ have them. This post walks through the API, the problems it solves, and where it slots into the LLM JSON repair workflow.
What changed in the reviver signature
Before, the reviver took (key, value). After, it takes (key, value, context). The third argument is { source } for primitives — the exact substring of the source text that produced this value. For objects and arrays the source argument is omitted (you only get it on leaves).
JSON.parse('{"price": 9007199254740993}', (key, value, { source }) => {
if (typeof value === 'number' && source !== String(value)) {
// The number lost precision on the way to a JS Number.
return BigInt(source);
}
return value;
});
// → { price: 9007199254740993n }Compare that to the old workaround: parse the entire document, walk it, and either accept the precision loss or write a hand-rolled JSON parser that produces BigInts. The reviver version is six lines, runs at native speed, and only escalates to BigInt when the round-trip actually drifts.
Where source-access actually helps
1. Lossless number parsing
Every JSON producer at integer-scale (Stripe IDs, Discord snowflakes, Postgres bigints) ships numbers that don't fit in a JavaScript Number. Without source access, parsing them silently loses precision. With source access, the reviver can detect a precision loss and rescue the original digits.
function parseWithBigInts(text) {
return JSON.parse(text, (_key, value, ctx) => {
if (typeof value !== 'number') return value;
if (Number.isInteger(value) && ctx.source !== String(value)) return BigInt(ctx.source);
return value;
});
}
parseWithBigInts('{"id":12345678901234567,"qty":2}');
// → { id: 12345678901234567n, qty: 2 }2. Verifying the exact string representation
JSON Canonicalization (RFC 8785) and many signature schemes care about the textual form of each value. The reviver can compare ctx.source against a canonical form and reject any document where, say, a number was serialized as 1.0 when the policy says it must be 1. Before, you needed a separate canonicalizer pass over the parsed AST.
3. Repairing LLM JSON output
LLMs frequently produce JSON that parses but with values that shouldn't have round-tripped through Number — phone numbers, account IDs, hex-encoded byte strings dressed up as numbers, ISO timestamps written without quotes. The reviver can spot these by comparing source against the parsed value, then either re-encode them as strings or reject the payload before downstream code commits to the wrong shape.
JSON.parse(llmOutput, (key, value, { source }) => {
// An account ID with the wrong type — keep the original digits as a string
if (key === 'accountId' && typeof value === 'number') return source;
return value;
});JSON.rawJSON for the round trip
The new JSON.rawJSON() factory lets you stringify a value back to its exact source form. Think of it as the inverse of the reviver's source access.
const big = JSON.rawJSON('12345678901234567');
JSON.stringify({ id: big });
// → '{"id":12345678901234567}'
// JSON.rawJSON validates the input — non-numeric strings throw
JSON.rawJSON('not-a-number');
// SyntaxErrorThe end-to-end shape is: parse with the reviver, manipulate as a JS value graph, then serialize. If any leaf was upgraded to a BigInt or wrapped in JSON.rawJSON, JSON.stringify emits the original digits verbatim.
What it doesn't fix
- Almost-JSON inputs. Source access is only handed to the reviver after a successful parse. If the LLM returned
{name: 'Ada',}with unquoted keys and trailing commas, JSON.parse throws before your reviver runs. You still need a repair pass first — see JSON Fix or the LLM repair guide. - String values. ctx.source for a string is the JSON-encoded form (with quotes and escapes), not the decoded characters. If you need the raw character data, use the parsed
value— it's already decoded. - Containers. Objects and arrays don't expose
ctx.source. The reviver fires on each leaf during the post-order walk, not on the wrapping nodes.
Browser + runtime support
The proposal reached Stage 4 of TC39 and landed in Chrome 114, Safari 18.2, Firefox 128, and Node 22. Web Platform Status tagged it Baseline 2025 once all evergreens picked it up, which is the cue browser-based tools have been waiting for. For older Node runtimes there's a polyfill, but the reviver signature change means a polyfill is more about feature detection than a true backport.
A practical workflow
Here's the order to apply this to a real LLM repair pipeline.
- Pass the LLM output through a repair step that handles fences, unquoted keys, single quotes, and trailing commas. Validate the cleaned text strictly — confirm it round-trips through
JSON.parsebefore going further. - Use the source-access reviver on the cleaned text to rescue any precision-loss numbers, lock down value-type expectations, and flag values whose serialized form drifted from policy.
- Hand the resulting graph to your app. When you need to send it back over the wire — to a downstream API or back to the model for follow-up — use
JSON.stringifyand any BigInts orJSON.rawJSONwrappers emit the original digits.
Source access doesn't replace the repair step — it makes the post-repair handling safer. Combined, you can take messy AI output and ship a typed, lossless graph downstream with native-speed parsing.
Related on this site:
- Hub: Repair LLM JSON output — full sequence from fence stripping to typed output.
- Guide: Repair LLM JSON output — concrete fixes for fences, quotes, and Python literals.
- Repair broken JSON in JavaScript — the same workflow in code.
- News: source-access reaches Baseline 2025