JSON Integer Precision Trap: When Snowflake IDs Are Silently Rewritten
Symptom: the ID is “almost” right
A class of bugs that wastes half a day:
- Backend logs show order/user id
2099774875233849345 - After a formatter, DevTools, or
JSON.parsein the app, it becomes2099774875233849400 - Signatures, reconciliation, and id lookups all fail — yet
JSON.parsedoes not throw
This is not truncation on the wire and not “formatting changed whitespace”. It is IEEE 754 double losing low bits while parsing a numeric literal.
Why valid JSON still loses precision
The JSON specs define a number as a decimal literal; they do not require arbitrary-precision integers. Browser and Node JSON.parse map numbers onto JS number (IEEE 754 binary64). Only integers within Number.MAX_SAFE_INTEGER (2^53−1 = 9007199254740991) round-trip bit-exact.
| Approx digits | Example | Risk |
|---|---|---|
| ≤15–16 | Ordinary autoincrement ids | Usually fine |
| 18–19 | Snowflake / some distributed ids | Silent rounding |
| Fractions / scientific | 1e20, 0.1 | Separate float semantics |
Console check:
JSON.parse('{"id":2099774875233849345}').id
// → 2099774875233849400
String(JSON.parse('{"id":2099774875233849345}').id) === '2099774875233849345'
// → false
Number.isSafeInteger(2099774875233849345) // → false
Snowflake-style ids pack timestamp, worker, and sequence into ~64 bits. That design lands squarely in the 18–19 digit band where binary64 mantissa (~53 bits) cannot represent every integer. Autoincrement ids that stay under ~16 digits usually survive; the moment a distributed id generator crosses the safe frontier, every JS consumer becomes a silent rewriter unless the wire format uses strings.
Language matrix: who rounds, who keeps digits
| Runtime / library | Default JSON number | Large integer fate |
|---|---|---|
Browser / Node JSON.parse | IEEE 754 double | Silent rounding past 2^53−1 |
Python json | Arbitrary-precision int | Digits preserved |
Go encoding/json into float64 | Double | Same trap as JS |
Go into json.Number / int64 | Text or 64-bit int | Preserved if typed correctly |
Jackson (Java) into long | 64-bit | OK within signed long; still fails if forced through JS |
The bug often looks “frontend-only” because the backend already held a correct 64-bit value. A proxy that re-encodes with a double-backed parser can corrupt the payload before it reaches the browser. Treat every hop that calls a stock JSON parser as a suspect.
Three exits (by reliability)
-
Protocol: IDs as strings
{"id":"2099774875233849345"}— the most portable long-term convention across languages. -
Debug path: lossless parse
Do not format or inspect with bareJSON.parse. This site’s JSON Formatter usesparseLosslessfor format/minify so numeric tokens keep their raw text. Useful when you need to see the payload before changing code. -
App layer: BigInt / decimal types
Only when you control both runtimes and the contract; the on-the-wire JSON often still carries strings so intermediate proxies do not re-parse into doubles.
How this links to other pages on the site
| Scene | Risk | See |
|---|---|---|
| Snowflake ids in API JSON | Frontend JSON.parse | This article + formatting guide |
| Large integers in JWT claims | Decode then parse | JWT decode guide |
| Nested JSON after Base64 | Encoding lossless, parse still lossy | Base64 guide |
| Diff says numbers differ | Parser corruption vs business change | Troubleshooting handbook |
Concrete chain: Base64 → JSON → JWT claim
A common pipeline: service A Base64-encodes a JSON blob, service B decodes bytes then JSON.parses, and a claim later lands in a JWT payload. Base64 never changes digit characters — the damage happens at parse. If you only inspect the Base64 tool output as text, the id still looks correct; the moment any hop materializes a JS number, the low bits are gone. Debug order: verify the raw JSON token (lossless formatter) → check whether JWT decode displays the claim as a string or a number → only then blame transport.
Implementer's note
The lossless path uses a hand-written recursive-descent parser, wraps number tokens in LosslessNumber{ raw }, and prints raw on stringify instead of Number(raw). A validate-only probe may still call stock JSON.parse — that answers “can it parse?”, not “were values preserved?”. Signing, hashing, and reconciliation must use strings or the lossless tree.
Quick checklist
- Paste the suspicious id into the JSON Formatter and compare digit strings to the original token.
- If the formatter preserves it but your app does not — search for bare
JSON.parse/response.json(). - Push the backend to emit string ids; short-term, keep strings or BigInt on the client.
- Do not “fix” by formatting again in a lossy tool — that freezes the wrong digits into the clipboard.
Triage: precision vs something else
| What you see | Likely cause | Next step |
|---|---|---|
| Last digits become 0 / round hundreds | IEEE 754 rounding | Lossless path here / string ids |
Whole field null / missing | Field name or serializer config | Schema / Diff |
Chinese becomes \uXXXX | ASCII-escape policy | Unicode escape guide |
Unexpected token | Syntax | Troubleshooting handbook |
| Diff shows number mismatch, no business release | One side used a lossy parse | Re-compare after lossless/string on both sides |
Money, FX, and inventory fractions are a different class (decimal vs binary float). Do not conflate them with “integer ids rewritten”. String ids for identifiers; integer minor units or decimal strings for money — both disciplines can hold at once.
FAQ
Does the lossless path keep float tokens verbatim too?
Yes for the literal text (0.50, 1e5). That is not decimal arithmetic precision. Money still belongs in integer cents or decimal strings.
Is this a JSON spec bug?
No. ECMA-404 / RFC 8259 leave number representation to implementations. The engineering mistake is carrying business ids in doubles, not indentation style.
Doesn’t “validation passed” contradict this?
No. Validation asks syntax; precision asks semantics. Green syntax plus a wrong id is the textbook picture of this trap.
This article is provided by ToolVault. Visit the homepage for more developer tools.
Related Tools
Related Articles
JSON Parse Error 'Unexpected token'? 5 Common Causes and How to Fix Them
Getting a JSON parse error 'Unexpected token'? This article covers the 5 most common causes of JSON format errors — trailing commas, single quotes, unescaped characters, BOM headers, and comments — with fixes and code examples for each.
JSON Troubleshooting Handbook: From Error Message to Fix (Symptom Index)
JSON errors that make no sense? This handbook organizes fixes by symptom — syntax errors, Unicode escaping, encoding mojibake, and Schema validation failures, with a diagnostic tree and a general debugging workflow.
JSON Schema Reports "required" Property Missing — How to Locate and Fix
JSON Schema validation says a required property is missing, or type mismatch? Explains required / additionalProperties / type pitfalls and how to locate the exact level with our validator.