JSON has one number type — and no precision rules
JSON's grammar defines a single number type and, crucially, says nothing about precision or range. It's just a sequence of digits with an optional fraction and exponent. That leaves the actual precision up to whatever parses the JSON — and in JavaScript (and most languages by default) that means IEEE 754 double-precision floating point. Doubles are binary, and binary can't represent every decimal exactly, which is where the surprises start. This is the decimal-precision companion to big integers in JSON, which covers the large-integer side of the same problem.
The famous example
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
JSON.parse('{"total": 0.3}') // 0.3 stored as the nearest doubleThis isn't a JavaScript bug — it's how binary floating point works in every language that uses IEEE 754 (Python, Java, Go, C, all do the same). The decimals 0.1, 0.2, and 0.3 have no exact binary representation, so each is stored as the *nearest* double, and the tiny rounding errors accumulate. The value round-trips through JSON fine; the issue is doing arithmetic on decimals that were never exact to begin with.
The other trap: the 2^53 integer limit
Doubles can represent integers exactly only up to 2^53 − 1 (Number.MAX_SAFE_INTEGER, 9007199254740991). Beyond that, integers silently lose precision:
JSON.parse('{"id": 9007199254740993}').id // 9007199254740992 ← wrong!The number parsed, no error was thrown, and the last digit is simply *wrong*. This bites hard with large IDs — Twitter/X snowflake IDs, database bigints, blockchain values — where a 64-bit integer from a backend gets mangled the moment JavaScript parses it. The data looks fine and is quietly corrupted.
How to store money, IDs, and exact decimals safely
The rule: don't put values that require exact precision into a JSON number. Use one of these instead.
- Money → integer minor units. Store
$19.99as1999cents (an integer within safe range) and divide only for display. Never do financial math on JSON floats. - Money/decimals → strings. Store
"19.99"as a string and parse with a decimal library (e.g. big.js, decimal.js) that does exact base-10 arithmetic. - Large IDs → strings. Serialize 64-bit IDs as
"9007199254740993". This is why so many APIs return IDs as strings — it's the only way to survive a JavaScript client. See big numbers in JSON for the BigInt-based approach. - Comparisons → use an epsilon. If you must compare computed floats, check
Math.abs(a - b) < Number.EPSILONrather than===.
{
"price_cents": 1999,
"price_display": "19.99",
"account_id": "9007199254740993"
}What JSON preserves and what it doesn't
- The text of a number is preserved across serialization —
JSON.stringify(0.3)gives"0.3". The precision loss happens when the string is parsed into a double and when you compute with it, not in the JSON text itself. - Trailing zeros and leading formatting are not preserved —
1.50parses to1.5, and1e3becomes1000. If exact formatting matters, use a string. - `NaN`, `Infinity`, and `-0` are not valid JSON numbers.
JSON.stringify(NaN)producesnull; you can't round-trip them as numbers.
The mental model: JSON numbers are perfect for *counts, quantities, and measurements where a little rounding is fine* — and wrong for *money, precise decimals, and large identifiers*, which belong in strings or integer minor units. For related handling see big numbers in JSON and the JSON.stringify guide.