JSON.parse Rounds Large Numbers: 64-bit IDs and Precision Loss
Most JSON bugs announce themselves with a SyntaxError. This one does not. JSON.parse() accepts the text, returns an object, and the number inside it is quietly a different number. A 19-digit ID ends in the wrong digits, a long decimal loses its tail, and nothing throws. The data is wrong from that point on, and the first symptom is usually a record that "can't be found" three services later. Every output below was produced in Node 22 before it went on this page, and each example opens prefilled in JSON Lab, so you can press Prettify and watch the digits change in your own browser.
9007199254740991 (Number.MAX_SAFE_INTEGER). Anything larger is rounded to the nearest representable value on the way in. Send big IDs as strings, or read them with a parser that keeps the raw text.
The rule: 253 − 1
A double has 53 bits of integer precision. Below 9007199254740991 every whole number is representable. Above it, the gaps start: first only even numbers, then multiples of four, and so on. A value that falls in a gap is replaced by a neighbour. Nothing in the JSON text marks the boundary, which is why a payload with ID 9007199254740991 works and one with ID 9007199254740993 does not.
| JSON text | After JSON.parse() then JSON.stringify() |
|---|---|
9007199254740993 | 9007199254740992 |
1234567890123456789 | 1234567890123456800 |
12345678901234567890 | 12345678901234567000 |
19.999999999999999999 | 20 |
1. The first integer past the safe limit
The smallest case that shows the problem is 2^53 + 1. It is off by one, which is the worst kind of wrong because it looks plausible:
JSON.parse('{"id": 9007199254740993}').id;
// 9007199254740992
No error, no warning, and Number.isSafeInteger() on the result returns false only if you think to ask. Two different IDs, ...992 and ...993, now parse to the same number, so a lookup table keyed on the parsed value can merge two records.
See it happen live
2^53 + 1 comes back as 2^53. Press Prettify and compare the digits:
Open this example in JSON Lab2. 64-bit database IDs and snowflakes
Auto-increment BIGINT columns, snowflake-style IDs and many event or order identifiers are 64-bit integers. They routinely exceed 253, and they are exactly the values that JSON APIs send as bare numbers. The browser or Node client then rounds them:
JSON.stringify(JSON.parse('{"id": 1234567890123456789}'));
// '{"id":1234567890123456800}'
The result is a perfectly valid ID that belongs to no record. Because the round trip also happens when you "just format" a payload, tools that parse and re-serialise (including a naive formatter) corrupt it too. This is why many public APIs return a second, string-typed copy of each large ID. If you control the API, make the string the primary field.
See it happen live
A 19-digit ID is rewritten by a plain parse and stringify:
Open this example in JSON Lab3. Integers wider than 64 bits
A 20-digit number does not even fit in 64 bits, so the loss is larger and the output switches to trailing zeros:
JSON.parse('{"n": 12345678901234567890}').n;
// 12345678901234567000
The same text is read exactly by Python's json.loads, which has arbitrary-precision integers, so a Python service can produce a value that a JavaScript client then breaks. When two systems disagree about a number that "looks the same", compare the raw text before blaming the network.
See it happen live
A 20-digit integer loses everything after the first 17 digits:
Open this example in JSON Lab4. Long decimals and money
Decimals have the same problem from the other side. A double keeps roughly 15 to 17 significant digits, so a value written with more digits than that is rounded:
JSON.parse('{"price": 19.999999999999999999}').price;
// 20
This one is rare in practice because few APIs emit 20 significant digits, but it explains why exact-decimal data (prices, tax rates, coordinates from some GIS exports) should not travel as JSON numbers if every digit matters. Send an integer count of the smallest unit (cents), or a decimal string, and do the arithmetic in a decimal type.
Not precision loss, but also not preserved
Number formatting is not part of the data model either. After parsing, 1.50, 1.0 and 1e3 become 1.5, 1 and 1000:
JSON.stringify(JSON.parse('{"a":1.50,"b":1.0,"c":1e3}'));
// '{"a":1.5,"b":1,"c":1000}'
This matters if you hash or sign the text, or diff a formatted file against the original: the values are equal, the bytes are not.
How to detect it
- Check
Number.isSafeInteger(value)on every ID-like field after parsing. Afalseon a field you expect to be whole means the digits may already be wrong. - Compare against the source text. If the raw JSON still exists, search it for the field. If the digits you see in the network panel differ from what your code logs, parsing is the culprit.
- Look at the length. Any bare integer with 16 or more digits in a payload is a candidate.
How to fix it
- Send large IDs as strings. This is the only fix that works everywhere, including old browsers and every client language. Change the producer:
{"id": "1234567890123456789"}. - Keep the raw text with a reviver. In Node 22,
JSON.parsepasses a third argument to the reviver with the originalsourcetext of each primitive:
That output is from Node 22. I did not test it in other engines, so check support before relying on it in a browser, and note thatconst reviver = (key, value, ctx) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(ctx.source) ? BigInt(ctx.source) : value; JSON.parse('{"id": 1234567890123456789, "n": 5}', reviver); // { id: 1234567890123456789n, n: 5 }JSON.stringifycannot serialise aBigIntwithout a custom replacer. - Use a parser that returns BigInt or strings for big values, such as the
json-bigintpackage, if you cannot change the producer or the runtime. - Do not repair after the fact. Once
JSON.parse()has run, the original digits are gone; there is nothing to recover.
Related: if the failure is a thrown error rather than a silent change, see Unexpected token in JSON, and for payloads that stop partway see Unexpected end of JSON input.
📋 Paste a payload and see what survives parsing
JSON Lab formats and validates entirely in your browser, so nothing you paste is uploaded. The optional AI Repair button is the exception: it sends your input to Google's Gemini API.
Open JSON Lab →← Back to Blog · See also: "[object Object]" is not valid JSON →