Why JSON Numbers Break: the IEEE 754 and 2^53 Trap
If you've seen a large integer ID from an API response print with its last few digits different from the original, that's most likely not a bug in the tool or the server. JavaScript (and most JSON parsers built on it) stores every number in one format: IEEE 754 double-precision float. This format can represent integers exactly only up to 2^53 (9,007,199,254,740,992, about 9 quadrillion). This guide explains why that boundary is 2^53 specifically, and how to work around this limit in practice, starting from the computation structure.
1. Why a float can't represent integers exactly without limit
An IEEE 754 double splits its 64 bits into three parts: 1 sign bit, 11 exponent bits, and 52 fraction (mantissa) bits. A number is stored as "sign × 1.fraction × 2^exponent", and the "1." is always implicit because a normalized binary number always has a leading bit of 1. The problem is that JavaScript has no dedicated integer type. 1, 3.14, and 9007199254740993 are all crammed into the same 64-bit double format, and the number of significant digits the fraction can hold is fixed by this one format. Whether decimal or integer, the precision available is exactly 52 bits (53 with the implicit bit).
2. Why 2^53 is that boundary specifically
Adding the implicit leading 1 bit to the 52 explicit fraction bits gives 53 bits of precision total. Every integer from 0 to 2^53 can be represented exactly within those 53 bits, no error. But the moment you exceed 2^53, the exponent steps up one level and the gap between representable values widens from 1 to 2. That is, 2^53 is represented exactly but the next integer, 2^53+1, doesn't exist on the representable grid, so it's rounded to the nearest even number, 2^53. This boundary value is exactly JavaScript's Number.MAX_SAFE_INTEGER (= 2^53−1 = 9,007,199,254,740,991), and this value and its neighbors (±1) are safely distinguished, but beyond that different integers can collapse into the same value.
9007199254740993 === 9007199254740992 returns true. Even though the two are different integers in code, they collapse into the same bit pattern the moment they're stored as double. In fact, running JSON.parse('{"id":9007199254740993}') gives a result object whose id value is 9007199254740992 — a different number from the original.
3. How to work around large integers in JSON in practice
This isn't a bug in a specific library or tool — it's a fundamental limitation of the JSON standard itself. RFC 8259 doesn't specify number-type precision and leaves it to each implementation, and this phenomenon arises because most languages chose double for convenience. In practice, database 64-bit auto-increment IDs, Snowflake IDs, and large integer fields in some payment/finance APIs commonly cross this boundary. The most widely used workaround is to pass values that could be problematic wrapped as a string rather than a JSON number type. Wrapping in double quotes — {"id":"9007199254740993"} instead of {"id":9007199254740993} — makes the parser treat it as a string so the original digits are preserved. JavaScript's BigInt type can represent integers past this range exactly, but standard JSON.parse() already converts number literals to double at parse time, so you have to convert the original string to BigInt yourself or use a separate parser like json-bigint.
4. Summary — the order to check when handling large integers
- Check the value's magnitude first: determine whether the integer you're handling exceeds 9,007,199,254,740,991 (2^53−1). DB bigint primary keys and Snowflake IDs are the typical cases.
- Stringify when designing an API: if you're designing the API yourself, emitting large integer fields as JSON strings from the start blocks precision loss at the source.
- When consuming an external API: if a large ID comes down as a number rather than a string, precision may already have been lost on the server side, so request a string field with the original value alongside it if possible.
Frequently Asked Questions
Q. Why do JSON numbers change value for large integers?
A. The JSON standard (RFC 8259) doesn't specify number-type precision. Most JSON implementations including JavaScript store every number as an IEEE 754 double-precision float, which can represent integers exactly only up to 2^53. Read a larger integer with JSON.parse() and it's rounded to the nearest representable value, differing from the original.
Q. Where does the number 2^53 come from?
A. An IEEE 754 double uses the 52 bits remaining after 1 sign bit and 11 exponent bits from its 64 bits as the fraction. A normalized number always has an implicit leading 1 bit, so it effectively has 53 bits of precision and can represent every integer from 0 to 2^53 exactly. Past 2^53 the gap between representable values grows larger than 1 and consecutive integers can't be distinguished.
Q. How do I safely send large integers through JSON?
A. The most common way is to wrap the number as a string rather than a JSON number type. For example, {"id":"9007199254740993"} instead of {"id":9007199254740993} — the parser treats it as a string so it's preserved with no precision loss. This is a common convention when putting a server/DB 64-bit ID or a Snowflake ID in an API response.
Q. Does using BigInt instead of JSON.parse solve it?
A. BigInt itself can represent integers past 2^53 exactly, but standard JSON.parse() always converts number literals to double, so precision is already lost at parse time. To handle it with BigInt you have to receive the value as a string in the original JSON and convert it with BigInt() yourself, or use a custom parser library like json-bigint. JSON.stringify() also can't serialize a BigInt value directly and throws a TypeError.