If an API sends you 12345678901234567890 as a JSON number, what does JSON.parse give you? I ran a few cases in Node v22.23.4 (V8) to see exactly where the digits go, and what the newer context.source and JSON.rawJSON options do about it.
What parse and stringify do to big integers
const s = '{"id":12345678901234567890,"small":9007199254740993,"safe":9007199254740991,"price":0.1,"f":1.0}';
const o = JSON.parse(s);
| Field in the JSON text | After JSON.parse
|
Number.isSafeInteger |
|---|---|---|
12345678901234567890 |
12345678901234567000 |
false |
9007199254740993 (2^53 + 1) |
9007199254740992 |
false |
9007199254740991 (2^53 - 1) |
9007199254740991 |
true |
1.0 |
1 after a round trip |
n/a |
9007199254740992 === 9007199254740993 is true in the same run, so two different ids can collapse into one. Nothing throws and nothing warns. The value is just a different number, and JSON.stringify(o) writes the changed digits back out.
Reading the original digits: reviver context.source
The reviver gets a third argument. For primitives it has a source field with the original text. I used it to turn unsafe integers into BigInt:
const r = JSON.parse(s, (k, v, ctx) =>
typeof v === "number" && !Number.isSafeInteger(v) && Number.isInteger(v)
? BigInt(ctx.source) : v);
Result in Node 22: id is a bigint printing 12345678901234567890, small is a bigint printing 9007199254740993, and price stays the number 0.1. The digits survive.
Writing them back: two traps
-
JSON.stringify(r)on that object throwsTypeError: Do not know how to serialize a BigInt. -
JSON.rawJSON("12345678901234567890")is how you write an exact number literal.JSON.stringify({ id: JSON.rawJSON("12345678901234567890") })gave{"id":12345678901234567890}.
MDN documents both: context.source on the JSON.parse page, and JSON.rawJSON() as "Baseline 2025, newly available". MDN's own note on the reviver says it runs after the value is parsed, so a plain reviver value has already lost precision. That matches what I saw.
What I would do
- If an id is a big integer, ask the API to send it as a string. This works everywhere.
- If you cannot change the API, read it with
context.sourceon a runtime that has it, and keep it as a string or BigInt, not a number. - Do not do arithmetic on ids. Compare them as strings.
- Add a test with
9007199254740993. It is the smallest value that fails.
Not tested
Browsers (I ran Node only), older runtimes without context.source or rawJSON, other JSON libraries, and large decimals like 0.1000000000000000055511151231257827. Check your own target runtime before relying on the reviver option.
Related: What an input type=number really accepts.
Top comments (0)