Large Integer IDs in JavaScript
JavaScript numbers are doubles: integers above 2^53 − 1 (16+ digits) are silently rounded by JSON.parse, so a 64-bit ID can change before your next request even though the network response looks right. Send long IDs as strings.
🌱 A rough idea, planted recently. Expect rough edges — it will grow.
On this page
[!abstract] The lesson An ID is a label, not a number. If it can be longer than 15 digits, send it as a string. JavaScript can’t hold integers above 9,007,199,254,740,991 exactly, and
JSON.parserounds them without any error.
What I saw
Long IDs (64-bit, from a Java back end) came back wrong when a page sent them in a request payload, even though the raw payload in the Network tab looked fine. The ID had been rounded inside the browser, before the request was built.
Why it happens
- Every JavaScript
numberis a 64-bit float (IEEE 754 double). It has 53 bits for the digits, so integers are only exact up toNumber.MAX_SAFE_INTEGER= 2^53 − 1 = 9,007,199,254,740,991 (16 digits). - Above that, not every integer can be stored, so the value snaps to the nearest one that can. A Java
longgoes up to about 9.2 × 10^18 (19 digits), far past the safe range. JSON.parseturns every JSON number into anumber. There is no error and no warning:
JSON.parse('{"id": 9007199254740993}').id // 9007199254740992 (off by one)
JSON.parse('{"id": 1234567890123456789}').id // 1234567890123456800
JSON.stringify({ id: 1234567890123456800 }) // '{"id":1234567890123456800}', so the wrong ID goes out
Why the raw payload looked fine
The trap is that the text and the value are different things:
- The server sends
{"id": 1234567890123456789}. The Network tab’s Response view shows that raw text, which is correct. fetch(...).then(r => r.json())parses it. The value in memory is now1234567890123456800. (DevTools’ Preview view andconsole.logalso show the parsed, rounded value.)- The app puts that ID in the next request.
JSON.stringifywrites the rounded number, and the server gets an ID that doesn’t exist, or worse, one that does.
So looking at the wire tells you what the server sent, not what your code holds. The bug lives in the gap between them.
The fix: IDs are strings
- Contract: the API sends 64-bit IDs as JSON strings,
{"id": "1234567890123456789"}. Write it into the API contract, so every client gets it right, not only the browser. - Back end (Java/Jackson): serialise
LongIDs as strings, per field with@JsonSerialize(using = ToStringSerializer.class), or globally by registeringToStringSerializerforLong.classandlong.classon theObjectMapper. Accept both a string and a number on the way in during the migration. - Front end (TypeScript): type them as
id: string. NeverNumber(id)orparseInt(id)them; compare and use them as strings. - Test it (Contract-Driven Development): a contract test with an ID above 2^53 (for example
9007199254740993) that round-trips unchanged.
If you can’t change the server
- Parse with the source text. The
JSON.parsereviver now gets a thirdcontextargument whosesourceis the original text (Chrome/Edge 114+, Firefox 135+; check Safari before relying on it):JSON.parse(text, (key, value, ctx) => key === "id" && !Number.isSafeInteger(value) ? ctx.source : value) - Or a lossless parser library (
lossless-json,json-bigint) for the affected responses. BigIntis only for doing arithmetic on huge integers. IDs don’t need maths, andJSON.stringifythrows on a BigInt, so strings are simpler.
Rules of thumb
- Related: Browser Fundamentals for Front-end Engineers · API Modelling · The Fastest JSON Parser Can Still Be Too Slow
- If a value is an identifier (IDs, account numbers, order refs, phone numbers), make it a string, whatever its digits.
- Numbers are for maths. Amounts that need exact decimals have the same problem in a different form (
0.1 + 0.2), so send money as a string decimal or as integer minor units. Number.isSafeInteger(x)is the quick check when debugging.
Sources
- MDN: JSON.parse(), including the reviver’s
contextargument. - Browser support: JSON source text access.
- TC39 proposal: JSON.parse source text access.
- #javascript
- #json
- #precision
- #api-design
- #bigint
- #jackson
- #web-development
↩ Linked from (0)
Nothing links here yet — it’s a fresh shoot.
↪ Links to (4)
Related by topic
- Growing
Compact, Not Cramped
Frontend craft - Growing
Grow the Interface with the User
Frontend craft - Seedcase study
Making Room for Career-Changers
Career - Seed
Prototype or Product? Scoping a Request Honestly
Career