← Todos os artigos

JSON.parse Source Access: context.source, BigInts, JSON.rawJSON, and LLM JSON

Use JSON.parse reviver context.source and JSON.rawJSON safely: feature-detect support, rescue large numeric IDs, serialize BigInts, and harden repaired LLM JSON output.

JSON.parse() used to give revivers only two values: key and value. That was enough for dates and small transforms, but it was too late for precision-sensitive numbers. By the time the reviver saw 9007199254740993, JavaScript had already rounded it into a Number.

Source access changes that. For primitive values, modern JSON.parse() revivers can receive a third argument, usually called context, with context.source: the exact JSON token that produced the parsed value.

That one extra string lets you answer questions the old API could not answer:

  • Did this integer lose precision during parsing?
  • Was the original token 1, 1.0, or 1e0?
  • Did an LLM return a numeric account ID that should have stayed a string?
  • Can I serialize a BigInt back as a JSON number literal without hand-concatenating JSON?

The short version: use context.source for inspection while parsing, and use JSON.rawJSON() for lossless primitive serialization when your runtime supports it. Still repair invalid JSON before parsing, still validate the final shape, and still feature-detect before relying on the new APIs.

Current Status and Feature Detection

The TC39 proposal for JSON parse source text access is Stage 4. MDN documents the JSON.parse() reviver context parameter and marks JSON.rawJSON() as a Baseline 2025 feature. That means the APIs are ready for modern browser code, but older browsers, embedded WebViews, and older Node runtimes may not have them.

Do not guess from a user agent string. Feature-detect:

function supportsJsonParseSource() {
  let seenSource = false;

  JSON.parse('{"n":9007199254740993}', (_key, value, context) => {
    if (typeof value === 'number' && context?.source === '9007199254740993') {
      seenSource = true;
    }
    return value;
  });

  return seenSource;
}

function supportsRawJson() {
  return typeof JSON.rawJSON === 'function' && typeof JSON.isRawJSON === 'function';
}

This matters in real projects. A developer on a current browser may have JSON.rawJSON(), while a CI job or serverless function still runs an older Node runtime. The code should choose a fallback deliberately instead of half-preserving data.

What Changed in the Reviver

The old reviver shape was:

JSON.parse(text, (key, value) => {
  return value;
});

The source-aware shape is:

JSON.parse(text, (key, value, context) => {
  return value;
});

For primitive values, context.source is the original JSON text for that value:

JSON.parse('{"a":1,"b":"x","c":true,"d":null}', (key, value, context) => {
  if (context) {
    console.log(key, value, context.source);
  }
  return value;
});

Typical leaf output:

a 1 1
b x "x"
c true true
d null null

Notice the string case. The parsed value for "x" is x, but context.source is "x" including the JSON quotes. If the source text used escapes, the source keeps the escaped JSON spelling.

Also notice the if (context) guard. The reviver still runs for objects and arrays, including the root object, and those container calls do not receive source text. Do not destructure the third argument directly:

// Fragile: this can throw when the reviver is called for an object or array.
JSON.parse(text, (key, value, { source }) => value);

Use an optional context parameter and guard it.

Reviver Order Still Matters

The reviver walks bottom-up. Child values are visited before their containing object or array. At the very end, the reviver is called for the root value with an empty string key.

That means this:

JSON.parse('{"user":{"id":123,"name":"Ada"}}', (key, value, context) => {
  console.log({ key, value, source: context?.source });
  return value;
});

Conceptually visits:

id -> primitive, source is "123"
name -> primitive, source is "\"Ada\""
user -> object, no source
"" -> root object, no source

Returning undefined from a reviver removes a property, just like before. When adding source-aware logic, keep the default return path boring:

return value;

That one line prevents accidental deletions.

The BigInt Problem Source Access Solves

JavaScript Number is safe for integers only up to Number.MAX_SAFE_INTEGER, which is 9007199254740991. JSON itself does not have that limit. It can contain a longer integer token:

{
  "id": 9007199254740993,
  "qty": 2
}

Classic parsing loses the exact ID:

const parsed = JSON.parse('{"id":9007199254740993,"qty":2}');
console.log(parsed.id);
// 9007199254740992

With context.source, you can rescue the original digits:

function parseJsonWithBigInts(text) {
  return JSON.parse(text, (_key, value, context) => {
    if (!context || typeof value !== 'number') return value;
    if (!Number.isInteger(value)) return value;

    const source = context.source;
    const plainInteger = /^-?(0|[1-9]\d*)$/.test(source);

    if (!plainInteger) return value;
    if (Number.isSafeInteger(value) && source === String(value)) return value;

    return BigInt(source);
  });
}

const data = parseJsonWithBigInts('{"id":9007199254740993,"qty":2}');
console.log(data);
// { id: 9007199254740993n, qty: 2 }

The regex is intentional. BigInt("1e21") throws, and decimals are not BigInts. If your data contains precision-sensitive decimals, money amounts, or scientific notation, preserve those as strings or use a decimal library after parsing.

When Strings Are Better Than BigInts

BigInt is useful for arithmetic and exact comparisons, but IDs are often not numbers in the product sense. A user ID, invoice ID, Discord snowflake, account number, or database primary key may be better as a string even if the producer sent digits.

Use key-specific rules when the contract says "this field is an identifier":

const idKeys = new Set(['id', 'userId', 'accountId', 'invoiceId']);

function parseIdsAsStrings(text) {
  return JSON.parse(text, (key, value, context) => {
    if (!context) return value;

    if (idKeys.has(key) && typeof value === 'number') {
      return context.source;
    }

    return value;
  });
}

const value = parseIdsAsStrings('{"accountId":12345678901234567,"total":42}');
console.log(value.accountId);
// "12345678901234567"

This is especially useful with LLM output. Models often emit account IDs, phone numbers, tracking numbers, and ticket IDs as bare numbers because they look numeric. The safer downstream representation is usually a string.

The best fix is still at the producer: ask for IDs as strings in the schema or prompt. Source access is a backstop when valid JSON still carries the wrong type.

Check Canonical Numeric Form

Sometimes you do not want to convert the value. You want to reject non-canonical spelling.

For example, a signing pipeline might require integers to be serialized without plus signs, decimals, or exponent notation. Source access lets you enforce that during parse:

function parseCanonicalIntegers(text) {
  return JSON.parse(text, (key, value, context) => {
    if (!context || typeof value !== 'number') return value;
    if (!Number.isInteger(value)) return value;

    const source = context.source;
    const canonical = /^-?(0|[1-9]\d*)$/.test(source);

    if (!canonical) {
      throw new SyntaxError(`Non-canonical integer at ${key || '<root>'}: ${source}`);
    }

    return value;
  });
}

This does not replace full canonicalization. If you need byte-for-byte canonical JSON for signatures, hashing, or cross-language verification, use a canonicalization standard such as RFC 8785. Source access simply gives your reviver the original primitive token before that information disappears.

JSON.rawJSON for Lossless Output

JSON.rawJSON() creates a special raw JSON object containing a piece of valid primitive JSON text. JSON.stringify() then emits that primitive text as JSON instead of converting it through a JavaScript Number.

const raw = JSON.rawJSON('9007199254740993');

console.log(JSON.stringify({ id: raw }));
// {"id":9007199254740993}

This is not general string concatenation. The argument must be valid JSON text for a primitive value:

JSON.rawJSON('123');      // number token
JSON.rawJSON('"hello"');  // string token, including JSON quotes
JSON.rawJSON('true');     // boolean token
JSON.rawJSON('null');     // null token

Invalid JSON text throws:

JSON.rawJSON('not-a-number');
// SyntaxError

Do not use JSON.rawJSON() to inject objects or arrays. It is designed for primitive tokens, with lossless number serialization as the main use case.

Serialize BigInts Without Losing Digits

Plain JSON.stringify() cannot serialize BigInt values:

JSON.stringify({ id: 9007199254740993n });
// TypeError

When JSON.rawJSON() is available, use a replacer:

function stringifyWithRawBigInts(value) {
  if (typeof JSON.rawJSON !== 'function') {
    throw new Error('JSON.rawJSON is not available in this runtime');
  }

  return JSON.stringify(value, (_key, item) => {
    if (typeof item === 'bigint') {
      return JSON.rawJSON(item.toString());
    }

    return item;
  });
}

const output = stringifyWithRawBigInts({ id: 9007199254740993n, qty: 2 });
console.log(output);
// {"id":9007199254740993,"qty":2}

If the receiver accepts string IDs, a simpler fallback is often better:

function stringifyBigIntsAsStrings(value) {
  return JSON.stringify(value, (_key, item) => {
    return typeof item === 'bigint' ? item.toString() : item;
  });
}

Choose the output shape from the API contract. Do not emit a giant JSON number just because the API exists. Many JavaScript clients will still lose precision if they parse that number without source-aware handling.

How This Fits an LLM JSON Repair Pipeline

Source access does not fix invalid JSON. The reviver runs only after strict parsing succeeds.

A realistic LLM pipeline looks like this:

raw model text
-> extract the JSON-looking block
-> repair almost-JSON syntax
-> validate strict JSON
-> parse with a source-aware reviver
-> validate the schema
-> pass the typed value to the app

Example:

const idLikeKeys = new Set(['accountId', 'invoiceId', 'ticketId']);

function parseRepairedLlmJson(text) {
  return JSON.parse(text, (key, value, context) => {
    if (!context) return value;

    if (idLikeKeys.has(key) && typeof value === 'number') {
      return context.source;
    }

    if (typeof value === 'number' && !Number.isSafeInteger(value)) {
      throw new SyntaxError(`Unsafe numeric literal at ${key}: ${context.source}`);
    }

    return value;
  });
}

This catches a common "valid but wrong" model output:

{
  "accountId": 12345678901234567,
  "action": "lookup"
}

The JSON syntax is valid, so a repair tool has nothing to fix. But the field is not safe as a JavaScript number. A source-aware reviver can keep the original digits as a string or reject the payload before an agent, workflow, or support tool uses the wrong account.

For syntax repair before this step, use JSON Fix, Repair LLM JSON Output, or your own jsonrepair pipeline. For runtime trust, add schema validation after parsing. Syntax repair, source-aware parsing, and schema validation solve different problems.

What Source Access Does Not Fix

Problem Does source access help? What to use instead
Trailing commas, single quotes, unquoted keys No. JSON.parse() throws before the reviver runs. JSON repair, stricter prompting, structured outputs
Missing required fields No. The JSON can parse and still be the wrong shape. JSON Schema, Zod, Pydantic, Ajv
Object or array source text Not directly. Context is for primitive values. A parser that preserves parse nodes or token spans
Decimal money precision Partly. You can see the original token, but Number may still be wrong. String decimals, decimal library, schema rules
Old runtimes Not reliably. The third argument may be missing. Feature detection and fallback behavior
Security decisions No. Exact source text is not authorization. Normal authz, validation, audit logging

Treat source access as a precision and inspection tool. It is not a sanitizer, not a schema validator, and not a permission system.

Fallback Strategy for Older Runtimes

Use three tiers:

function parseWithPrecisionPolicy(text) {
  if (supportsJsonParseSource()) {
    return parseJsonWithBigInts(text);
  }

  const value = JSON.parse(text);

  // Old runtime fallback: reject known-risk numeric patterns before trusting
  // the parsed value, or require producers to send IDs as strings.
  if (/"(?:id|userId|accountId)"\s*:\s*-?\d{16,}/.test(text)) {
    throw new Error('This runtime cannot parse long numeric IDs losslessly');
  }

  return value;
}

That regex is not a JSON parser. It is only a conservative guard for known-risk inputs when the runtime lacks source access. For serious lossless parsing on old runtimes, use a dedicated lossless JSON parser or require the producer to quote precision-sensitive values.

Practical Rules

Use source access when:

  • you parse untrusted or model-generated JSON that may contain long numeric IDs,
  • you need to preserve exact primitive spelling,
  • you are building browser-local JSON tooling,
  • you want to reject unsafe numeric literals early,
  • you can feature-detect and fall back cleanly.

Avoid it as the first tool when:

  • the input is not valid JSON yet,
  • your only problem is formatting or indentation,
  • the downstream contract already sends IDs and decimals as strings,
  • you need full source spans for objects and arrays,
  • old runtimes must behave exactly like new runtimes.

My default recommendation for new API contracts is still simple: send IDs, account numbers, hashes, phone numbers, and decimal money as strings. Use context.source for defensive parsing at the boundary, not as an excuse to publish fragile contracts.

Frequently Asked Questions

What is JSON.parse source access?

It is the newer reviver behavior where JSON.parse() can pass a third context argument for primitive values. context.source contains the original JSON token text that produced that value.

Why does context.source matter for large numbers?

JavaScript can round large JSON numbers before your code sees them. context.source preserves the original digits, so a reviver can convert the token to BigInt, keep it as a string, or reject it.

Is context.source passed for objects and arrays?

No. The source context is for primitive values such as strings, numbers, booleans, and null. Guard context before reading it because object, array, and root-container reviver calls may not receive source text.

Does source access repair invalid JSON?

No. The reviver runs only after JSON.parse() succeeds. Fix trailing commas, single quotes, unquoted keys, comments, and markdown fences before using source-aware parsing.

What is JSON.rawJSON() for?

JSON.rawJSON() lets JSON.stringify() emit an exact primitive JSON token, such as a long integer, instead of forcing it through a JavaScript Number.

Can JSON.rawJSON() output objects or arrays?

No. It is for valid primitive JSON text, such as a number, string, boolean, or null. It is not a general JSON injection escape hatch.

How do I detect support safely?

Call JSON.parse() on a small sample and check whether the reviver receives context.source. Separately check typeof JSON.rawJSON === "function" and typeof JSON.isRawJSON === "function".

What should I do in older Node or browsers?

Feature-detect and fall back. Either require producers to send precision-sensitive values as strings, reject known-risk long numeric IDs, or use a dedicated lossless JSON parser for old runtimes.

Related on This Site

Sources

Last reviewed July 2026.