Why an ID should be a string in JSON
A string. JSON has one numeric type, and nearly every parser reads it into a double-precision float, which represents integers exactly only up to 2^53 − 1 — about 9.007 × 10^15. A 64-bit database ID can exceed that, and when it does the parser silently returns the nearest representable value instead. The record is then fetched, compared, or deleted by an identifier that is off by one, with no error anywhere in the chain.
What actually goes wrong
JSON.parse("9007199254740993") returns 9007199254740992. The value is one less than what was sent, and nothing in the process reports a problem: the JSON was valid, the parse succeeded, and the number is a perfectly ordinary number. It is simply not the number that was serialised.
That threshold — 9,007,199,254,740,991 — is Number.MAX_SAFE_INTEGER. Below it, every integer round-trips exactly. Above it, the gaps between representable values grow, and two distinct IDs can collapse onto the same double. Snowflake IDs, Twitter status IDs, Kafka offsets, and any bigint primary key are routinely above it.
The failure is quiet by construction. There is no exception to catch and no flag to check, because from the parser's point of view nothing unusual happened. The first visible symptom is usually a record that cannot be found, or worse, the wrong record being returned.
Why the specification allows it
RFC 8259 does not put a limit on the size of a JSON number — the grammar accepts any sequence of digits. What it does instead is warn that interoperability is only assured within the range that IEEE 754 double precision represents exactly, because that is what implementations actually use.
So a serialiser that writes a 64-bit integer as a JSON number is producing valid JSON. The document is correct; the round trip is not. The specification is telling you that correctness of the document and correctness of the value are two different questions.
What to do instead
Serialise the identifier as a JSON string. A string round-trips exactly at any length, sorts predictably, and costs two extra bytes. Every language that consumes the API already has a string type that will not quietly change the value.
This is what the APIs that ran into the problem now do. Twitter added id_str alongside id after the numeric field broke JavaScript clients; Stripe, Discord, and most systems built afterwards send identifiers as strings from the start.
If you are on the receiving end and cannot change the producer, parse with something that preserves the value — a reviver that reads large integers as BigInt, or a parser configured to return numbers as strings — before the double conversion happens. Once JSON.parse has run, the original value is gone and cannot be recovered.
The same reasoning applies to anything that is an identifier rather than a quantity: account numbers, order references, and zero-padded codes. If you will never do arithmetic on it, it is not a number.
Tools for this
- JSON ViewerValidate, format, and explore JSON as a tree or a table. Large API responses stay foldable.
- CSV to JSON ConverterTurn a spreadsheet export into a JSON array, with numbers and booleans typed correctly.
- JSON to CSV ConverterTurn a JSON array into a spreadsheet, flattening nested objects and keeping every field.
Specifications this follows
- RFC 8259 §6 — Numbers — States that interoperability is only assured within IEEE 754 double-precision range.
- MDN — Number.MAX_SAFE_INTEGER — The 2^53 − 1 threshold and what happens above it.
- IEEE 754 — Floating-Point Arithmetic — The representation every mainstream JSON parser uses for numbers.
Updated 2026-09-05