64-bit IDs break the moment they pass through JSON.parse

A 19-digit ID goes in, a 19-digit ID comes out, and it is a different number. Here is why, and what to do about it.

September 20, 2026 · 3 min read

Paste this into any JavaScript console:

JSON.parse('{"id": 1276898186410434561}')

You get 1276898186410434600. Not a rounding of the display — the stored value is genuinely different. The last two digits are gone, and if you then use that ID to look up a record, you get a 404 that is very hard to explain.

Why it happens

JSON has one numeric type. There is no distinction between integers and floating-point numbers in the specification, so a conforming parser is free to represent everything as a double. JavaScript’s Number is an IEEE-754 double, which represents integers exactly up to 253 − 1 — 9,007,199,254,740,991, sixteen digits.

Snowflake-style IDs, Twitter/X post IDs, and 64-bit database primary keys routinely exceed that. 1276898186410434561 is about 1.28 × 1018, roughly 140 times larger than the safe integer limit. The low-order bits are simply not representable, so they are lost.

The trap is that nothing warns you. JSON.parse succeeds, JSON.stringify round-trips the damaged number happily, and the corruption only shows up later as a mismatched lookup or a duplicate key.

How to spot it

You are exposed if any of these are true:

  • The API returns IDs as bare numbers rather than strings, and they are 17 digits or longer.
  • You comparing an ID received from your backend against one typed in by a user, or against a value from a different service.
  • You use JavaScript or TypeScript anywhere on the path, including a Node service that proxies the payload.
  • The ID ends in one or more zeros more often than chance would suggest. Trailing zeros are the fingerprint of precision loss.

A quick check: if id and id + 1 are equal in your runtime, the value is beyond the safe range.

Fix 1: keep big IDs as strings (the right answer)

The API should serialise 64-bit identifiers as JSON strings:

{ "id": "1276898186410434561" }

A string survives every JSON parser in every language, because there is nothing to round. If you control the service, change the serialiser to emit IDs as strings and treat the type change as a breaking API version. If you do not control it, parse the response with a JSON parser that can preserve number literals losslessly, or fetch the field as text through a schema-aware client.

In Go, this is the json.RawMessage or string route. The JSON to Go converter on this site generates string fields for large integers for exactly this reason, and flags when a value in your sample exceeds the safe range so you can see the risk in your own payload rather than discovering it in production.

Fix 2: parse with BigInt only when you must

JSON.parse has a reviver argument, and it can convert a long number literal into a BigInt before precision is lost — but only if you get to the raw text, which the reviver does not provide. The reviver receives the already-parsed value, so by then the damage is done.

The workaround is a two-pass parse: regex the raw text for digits longer than fifteen characters outside of string context, quote them, then JSON.parse the result. Use this only for one-off tooling. It is easy to get wrong on escaped strings and nested quotes, and it changes the type of the field, which means the rest of your code has to handle BigInt — which does not serialise back to JSON without a custom replacer.

Most teams are better off fixing the API.

The general lesson

The precision cliff is not specific to IDs. It applies to any 64-bit integer in a JSON payload: database sequences, nanosecond timestamps, monetary values in minor units, and hash-like numeric keys. Floating-point money is a separate and equally well-known hazard for the same underlying reason.

If a number can exceed 253, treat it as text on the wire and parse it into a precise type at the edges of your system. Interrogate any JSON payload with a formatter that reports key counts and depth, and check the shape of IDs before they reach your application code — the failure is much cheaper to catch in a sample response than in a production lookup.

Try it, without uploading anything

JSON to Go Struct — Turn a JSON response into Go structs with json tags. Open the tool.