[readonly] markdown buffer
Lossless JSON With JSON.parse Source Text Access
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
valuefor normal JSON - use
context.sourcewhen the exact token matters - use
JSON.rawJSONonly 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.