JSON.parse loses precision because it turns every JSON number into a JavaScript Number, which is an IEEE 754 double. A double can only hold integers exactly up to 9007199254740991 (2^53 - 1). Anything bigger, like a 64-bit database ID, gets rounded to the nearest value a double can store, and JSON.parse does it without an error or a warning.
A real example
The X (formerly Twitter) developer docs explain that X IDs are 64-bit unsigned integers, that "in JavaScript, integers are limited to 53 bits", and that an ID like 10765432100123456789 comes out as 10765432100123458000 when read as a number. Look that up and you get the wrong post, or nothing.
Any browser console shows it:
JSON.parse('{"value": 9223372036854775807}').value
// 9223372036854776000
That's the largest signed 64-bit integer, and the result is the example from the json-bigint README. The JSON text was fine; the parser changed the digits.
How it works
JSON puts no size limit on numbers. RFC 8259, section 6 says outright that most software uses IEEE 754 binary64 doubles, and that implementations "will approximate JSON numbers within the expected precision". It also spells out the safe zone: integers in the range [-(253)+1, (253)-1] are the ones implementations "will agree exactly on".
A double stores 52 bits of mantissa, plus an implicit leading 1, so 53 bits of precision. MDN's page on Number.MAX_SAFE_INTEGER shows what happens one step past the edge: Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 is true. Past 2^53, neighboring integers share a double.
The TC39 proposal for JSON source text access puts it bluntly: the conversion between JavaScript values and JSON text "is lossy". Its own example: 999999999999999999, 999999999999999999.0 and 1000000000000000000 all parse to the same number.
When it matters
Mostly with IDs: Snowflake-style IDs, 64-bit database keys, anything a server generates as a 64-bit integer. If the backend reads those values with a 64-bit integer type, the backend team never sees a problem. The bug shows up in the JavaScript client, or in a Node service sitting in the middle.
It also matters for money and measurements with many decimal places. RFC 8259 points to 3.141592653589793238462643383279 as a number that signals trouble: the sender expects more precision than most receivers have.
If every integer you send stays under 9007199254740991, you'll never see this.
How to fix it
Send big numbers as strings (best)
If you control the API, put large IDs in quotes. The X API added id_str next to id for this reason, and its docs say to always use string IDs. The json-bigint README says the same: "it's a good idea to serialize values which might fall out of IEEE 754 integer precision as strings in your JSON api".
Read the original digits with context.source
If you can't change the API, newer engines give the reviver a third argument. MDN's JSON.parse page explains that the reviver runs after the number is converted, so value has already lost precision. But for primitive values, context.source holds the original text:
const obj = JSON.parse('{"gross_gdp": 12345678901234567890}', (key, value, context) =>
key === "gross_gdp" ? BigInt(context.source) : value
);
The proposal behind this is at stage 4. MDN notes that some parts of JSON.parse have varying support, so check your target browsers and runtimes, or use the core-js polyfill it links to.
Use a big-number parser
json-bigint is a drop-in parse/stringify that keeps every digit: 9223372036854775807 stays 9223372036854775807. It can return native BigInt (useNativeBigInt) or keep the value as a string (storeAsString).
Writing it back out
BigInt brings its own trap. Per MDN's BigInt reference, JSON.stringify throws a TypeError on any BigInt value. Handle it with a replacer, a toJSON method, or the proposal's JSON.rawJSON, which writes the digits as a bare number:
JSON.stringify(obj, (key, val) =>
typeof val === "bigint" ? JSON.rawJSON(String(val)) : val
);
Common mistakes
- Calling
BigInt(value)in the reviver. That gives you a BigInt of the already-rounded number. Usecontext.source, or don't parse withJSON.parseat all. - Trusting a formatter's output. A tool that parses and re-prints with
JSON.parsecan display the rounded number. Compare the raw text if an ID looks off. - Expecting an error. A huge number is valid JSON, so nothing fails. Compare
Unexpected end of JSON input, trailing commas or single quotes, which all stop the parse. - Switching to a JSON superset. JSON5 and JSONC change the syntax, not how parsers store numbers.
My take
Treat any ID generated outside JavaScript as a string, even if current values fit. IDs grow, and the day one crosses 2^53 the bug arrives without an error, in production, as "wrong record" reports that are miserable to trace. Strings cost a pair of quotes.
A limitation of checking in the browser
The OnlineViewer Dev JSON validator uses JSON.parse, so it answers "is this valid JSON?", not "will my numbers survive?". A 64-bit ID passes validation because it is valid JSON. To catch rounding, compare the digits in the raw text with what your code reads back.
FAQ
What is the largest integer JSON.parse keeps exactly?
9007199254740991, which is Number.MAX_SAFE_INTEGER (2^53 - 1). Above that, some integers can no longer be told apart and get rounded to a neighbor.
Is a 64-bit integer valid JSON?
Yes. The JSON grammar puts no limit on number size. The problem is the parser: JSON.parse stores numbers as IEEE 754 doubles, and RFC 8259 only promises exact agreement for integers within plus or minus 2^53 - 1.
Can a reviver function fix the rounding?
Not with the value argument, because the reviver runs after the number is already converted. In engines that support it, the reviver's third argument has context.source with the original digits, which you can pass to BigInt.
Why does JSON.stringify throw on BigInt?
BigInt values aren't serialized by default, so JSON.stringify raises a TypeError. Convert them yourself with a replacer, a toJSON method, or JSON.rawJSON.
What is the best fix for large IDs in an API?
Send them as strings. X does this with id_str for its 64-bit IDs, and the json-bigint README gives the same advice.