Preserve Large JSON IDs with Decimal Strings in JavaScript
A player profile loads successfully, but opening that player’s details returns the wrong record. The API sent a large integer ID, and JavaScript rounded it while parsing the response. Converting the parsed value to a string afterward preserves the rounded digits.
For identifiers that can exceed JavaScript’s safe integer range, define a string field in the API contract and keep it as a string through parsing, lookup, and serialization. This example uses positive unsigned 64-bit player IDs; adapt the validation rules to your own identifier format.
Reproduce the Precision Loss
Run this example in a JavaScript console or Node.js:
The maximum safe integer is 2 ** 53 - 1, or 9007199254740991. Beyond that range, different mathematical integers can map to the same JavaScript Number. Some larger integers remain exactly representable, but consecutive integers are no longer reliably distinguishable. See the
ECMAScript definition of Number.MAX_SAFE_INTEGER
.
An ordinary JSON.parse call produces Number values for JSON numbers. Calling String, toFixed, or BigInt on an already rounded value cannot recover the original digits. A two-argument reviver that receives only the parsed value has the same limitation. Runtimes supporting the reviver’s context.source can expose the original token, as documented in the
JSON.parse reference
; a quoted string contract avoids depending on that feature.
Send the Identifier as a JSON String
Have the server send this shape:
The quotes are part of the JSON representation. They must be present before the browser parses the response, including when using response.json(). The backend must serialize from its exact integer or string representation; converting an already rounded floating-point value to text on the server is too late as well.
Use the same string representation for small and large IDs. A field that alternates between a number and a string creates unnecessary branches and inconsistent map keys. In TypeScript, declare this API field as string, then validate incoming data at runtime; a type annotation does not change how JSON is parsed.
This contract applies specifically to identifiers. Counts, coordinates, and other numeric fields need their own range and arithmetic rules.
Validate the ID at the Application Boundary
For this example, valid player IDs range from 1 through 18446744073709551615, the maximum unsigned 64-bit integer. The canonical form contains ASCII decimal digits, has no leading zeros, and allows no sign or whitespace.
| |
The range check uses string length first. For equally long strings containing only ASCII digits, lexicographic order matches numeric order. No floating-point conversion is needed. Rejecting numeric input also catches a server regression even when a particular small ID would still be safe.
Do not silently trim, call parseInt, or remove leading zeros at this boundary. Those operations would accept values outside the chosen contract. If another system treats "0012" as a distinct identifier, define a different validator and preserve those zeros.
Keep Lookups and Outgoing JSON Exact
Use the validated string consistently:
| |
Avoid Number(playerId), unary +, and numeric coercion in shared helpers. A Map distinguishes string keys from number keys, so mixing representations can also cause a lookup to fail without any rounding.
If an integration requires a BigInt, construct it directly from the validated string and convert it back explicitly at the JSON boundary:
By default, serializing a BigInt directly with JSON.stringify throws a TypeError; see the
BigInt JSON guidance
. Explicit field conversion keeps this API contract visible. Most ID lookups need no integer conversion at all.
Test the Boundary and the Round Trip
Save the two validation functions above in a Node.js module, then append these assertions and run it with node:
| |
Use an .mjs extension so Node.js treats the file as an ES module. Include a real server response in integration tests as well: a client-only round trip cannot prove that the backend emits quoted, exact IDs. Inspect the raw response body when diagnosing a mismatch, then compare the ID at parsing, cache lookup, and outgoing request boundaries.