Skip to main content
ZEKILO Dev
Format

Why Big Numbers in JSON Change: 2^53 and BigInt

Updated 2026.10.03 · 4 min read

Large integer IDs in JSON lose digits in JavaScript because numbers are IEEE 754 doubles, exact only up to 2^53 − 1. Keep them exact with string IDs or BigInt.

Open JSON Formatter & Validator

If a server sends 12345678901234567890 and your page shows 12345678901234567000, the JSON is not broken. JavaScript reads every number as a 64-bit floating-point value (an IEEE 754 double), and integers are exact only up to 2^53 − 1, which is 9,007,199,254,740,991. The dependable fix is to send large IDs as strings. To see whether a document contains such numbers, paste it into the JSON Formatter & Validator: it keeps every number exactly as written and tells you how many would change if JavaScript read them.

What happens

JSON.stringify(JSON.parse('{"n":12345678901234567890}'));
// '{"n":12345678901234567000}'

JSON.parse("9007199254740993");
// 9007199254740992

There is no error and no warning; the value is simply different. When an order number or a post ID that was generated as a 64-bit integer changes like this, lookups fail or point to a different record.

Why the limit is 2^53

A JavaScript Number uses the IEEE 754 double-precision (binary64) format. Of its 64 bits, 1 is the sign, 11 are the exponent and 52 are the fraction; together with the implicit leading bit that gives 53 bits of precision. Every integer up to 2^53 can therefore be stored exactly. Between 2^53 and 2^54 only even integers can be represented, and the gaps keep growing as the numbers get larger. That is why 9007199254740993 becomes 9007199254740992.

Value Meaning
9,007,199,254,740,991 2^53 − 1, Number.MAX_SAFE_INTEGER
9,007,199,254,740,992 2^53; from here on, neighboring integers cannot be told apart
9,223,372,036,854,775,807 2^63 − 1, the largest signed 64-bit integer (19 digits)

2^53 has 16 digits. An integer with 17 or more digits is always outside the exact range, and a 16-digit integer needs to be compared with 9,007,199,254,740,991.

What the standard says

The number grammar in section 6 of RFC 8259 has no limit on the number of digits, so 12345678901234567890 is valid JSON. The same section allows implementations to set limits on the range and precision of the numbers they accept, and notes that good interoperability is achieved when implementations expect no more precision or range than IEEE 754 binary64 provides. Integers from −(2^53) + 1 to 2^53 − 1 are the range in which implementations agree exactly on the value.

So the change happens on the reading side. Languages that read the number into a 64-bit integer type or an arbitrary-precision integer keep the value (the 20-digit example above fits only an unsigned 64-bit integer), while JavaScript’s JSON.parse reads it into a Number. 1E400, which the RFC gives as an example of a potential interoperability problem, becomes Infinity in JSON.parse and then null when passed to JSON.stringify.

Fractions and notation change too

  • 1.0 becomes 1 after a round trip through JSON.parse and JSON.stringify. The value is equal but the text is not, which matters when the original text is compared or signed.
  • A fraction with many digits becomes the nearest double. 0.1234567890123456789 turns into 0.12345678901234568.
  • For values that must be exact, such as amounts of money, send a string or an integer in the smallest unit (cents, for example).

How to keep the value

Send IDs as strings. {"id":"12345678901234567890"} survives every parser. An ID is never used in arithmetic, so it has no reason to be a number. Change the server’s serializer settings so that 64-bit integers are written as strings.

Use BigInt when you need arithmetic. BigInt("12345678901234567890") keeps full precision. Note that JSON.stringify throws a TypeError when it meets a BigInt, so convert it with toString() before you serialize.

Read the source text when the value has to stay a JSON number. The TC39 proposal “JSON.parse source text access” (stage 4) passes the original text of each number to the reviver function as a third argument. As of October 2026 we confirmed that the code below works in Node.js 24.14.0. In a runtime without this feature the context argument is not passed, so check for it first.

const data = JSON.parse('{"n":12345678901234567890}', (key, value, context) =>
  typeof value === "number" && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source)
    ? BigInt(context.source)
    : value,
);
// data.n === 12345678901234567890n

Be careful when you only want to look at the data. A formatter that reads with JSON.parse and writes the result back changes these values just by re-indenting them. The ZEKILO Dev JSON Formatter & Validator does not compute with numbers; it copies them character by character. Formatting {"n":12345678901234567890} leaves the value untouched and shows “1 number(s) would change if read as a JavaScript Number (precision). They are kept exactly as written here.” 1.0 also stays 1.0.

Summary

  • The JSON standard does not limit the size of numbers, but JavaScript cannot store every integer above 2^53 − 1 exactly.
  • Exchange integer IDs with 17 or more digits as strings.
  • When JavaScript has to work with large integers, use BigInt and convert to a string when serializing.
  • To inspect JSON by eye, use a tool that does not rewrite numbers, such as the JSON Formatter & Validator.

Tools for this guide

Sources

More guides