XSCREENSAVER / 3D PIPES

[readonly] markdown buffer

Lossless JSON With JSON.parse Source Text Access

Mar 16, 2026 · 5 min read

JavaScript can lose digits before a JSON.parse reviver gets a chance to help. Source-text access fixes the parsing side, while JSON.rawJSON provides a matching route back out.

The precision was already gone

const input = '{"invoiceId":12345678901234567890}';
const parsed = JSON.parse(input);

parsed.invoiceId;
// 12345678901234567000

Turning the reviver's value into a BigInt does not recover the original literal; value has already passed through JavaScript's floating-point number representation.

Parse from the source text

For primitive values, a reviver can receive a third context argument. context.source contains the exact JSON token:

const parsed = JSON.parse(input, (key, value, context) => {
  if (key === 'invoiceId') {
    return BigInt(context.source);
  }

  return value;
});

parsed.invoiceId;
// 12345678901234567890n

Objects and arrays do not receive source text, and reviver traversal remains bottom-up. That is enough for this problem because the lossy values are primitive leaves.

Emit an exact numeric literal

Plain JSON.stringify() rejects BigInt. JSON.rawJSON() lets a replacer supply valid primitive JSON text without converting it to number first:

const wire = JSON.stringify(
  { invoiceId: 12345678901234567890n },
  (key, value) => typeof value === 'bigint'
    ? JSON.rawJSON(value.toString())
    : value
);

// {"invoiceId":12345678901234567890}

JSON.rawJSON() is intentionally narrow. It accepts a primitive literal such as a number, string, Boolean or null, not an arbitrary object or array fragment.

Keep the domain contract explicit

Not every large integer should become a BigInt. Many numeric-looking values are identifiers and are cleaner as strings because nobody should perform arithmetic on them.

Use a schema or an explicit list of fields to decide which values need lossless numeric handling:

const bigintFields = new Set(['invoiceId', 'ledgerSequence']);

function reviveKnownBigInts(key, value, context) {
  if (bigintFields.has(key) && /^-?\d+$/.test(context?.source ?? '')) {
    return BigInt(context.source);
  }

  return value;
}

A blanket rule that upgrades every integer changes application semantics and can surprise ordinary calculations.

Feature-detect the round trip

At the time of writing, JSON.rawJSON does not have universal support. Keep the behaviour behind a small helper and use a string fallback where broad compatibility matters.

The important distinction is simple:

  • use the parsed value for normal JSON
  • use context.source when the exact token matters
  • use JSON.rawJSON only when the receiver requires a numeric JSON literal

This does not make huge JSON numbers a good API design. It gives JavaScript a standard, lossless path when an existing contract already uses them.

Sources