Suppose an API returns an order ID, database primary key, snowflake ID, timestamp, or high-precision amount. The response is valid JSON and the request completed normally. Yet after JSON.parse(), the value no longer matches the server output. This is not a JSON syntax error. It is a mismatch between JSON’s number grammar and JavaScript’s default numeric type.
Reproduce JSON.parse precision loss
const json = '{"id":9007199254740993}';
const parsed = JSON.parse(json);
console.log(parsed.id); // 9007199254740992
console.log(parsed.id === 9007199254740993); // true
console.log(Number.isSafeInteger(parsed.id)); // false The second line looks impossible until you remember that the numeric literal on the right is rounded too. Both sides become the same representable JavaScript number. Comparing the parsed value with another unsafe Number therefore cannot prove that the original digits survived.
Why 253 − 1 is the boundary
JavaScript’s ordinary Number follows the IEEE 754 double-precision floating-point format. It has 53 bits of integer precision, including the implicit leading bit. That lets every integer from -(2^53 - 1) through 2^53 - 1 be represented exactly.
Number.MAX_SAFE_INTEGER // 9007199254740991 Number.MAX_SAFE_INTEGER + 1 // 9007199254740992 Number.MAX_SAFE_INTEGER + 2 // 9007199254740992 ← same result
Above that boundary, representable values become spaced farther apart. JavaScript must choose a nearby value, so different integers can collapse into the same Number. The same general issue affects long decimal fractions and exponents outside the finite Number range.
The MDN reference for Number.MAX_SAFE_INTEGER explains the safe integer boundary and its relationship to double-precision numbers.
Does JSON itself limit integer precision?
JSON defines a textual number syntax, not a universal machine number type. The digits 900719925474099312345 form valid JSON. Precision is decided by the parser and the destination language. A Java BigInteger, a database decimal type, and a JavaScript Number do not have the same range.
This is why a payload can remain exact on the backend and change only when a browser or Node.js process parses it. “Valid JSON” and “exactly representable as a JavaScript Number” are separate questions.
Why a normal JSON.parse reviver is usually too late
JSON.parse(source, (key, value) => {
if (key === "orderId") {
return BigInt(value);
}
return value;
}); In the traditional reviver workflow, the numeric token has already been converted to a Number before your callback receives it. Turning that rounded Number into a BigInt preserves the rounded value—not the original digits. A regex pass that quotes numbers before parsing can also break on strings, escapes, exponents, and nested syntax unless it is a real JSON tokenizer.
Four reliable solutions
1. Encode identifiers as JSON strings
{
"orderId": "900719925474099312345",
"createdAtNs": "1753859012345678901"
} This is usually the safest API contract for identifiers. IDs are labels, not quantities: clients do not need to add or divide them. Strings also work consistently across browsers, languages, databases, and logging systems.
2. Convert a quoted integer to BigInt when arithmetic is required
const data = JSON.parse(
'{"balance":"900719925474099312345"}'
);
const balance = BigInt(data.balance); BigInt represents arbitrarily large integers exactly. It does not represent decimal fractions, and JSON.stringify() does not serialize BigInt values by default. Define an explicit wire format—commonly a decimal string—when sending the value again.
3. Use a lossless JSON parser
When you cannot change the producer, use a parser that retains the original numeric token or maps it to an arbitrary-precision representation. This is the strategy JSONDock uses: formatting and tree exploration do not first squeeze every number through JavaScript Number.
4. Use decimal strings or a decimal library for money
BigInt only solves integers. Values such as 0.1 or 1234567890.123456789 need a decimal-aware strategy. Financial APIs commonly transmit minor units as integer strings or transmit decimal strings interpreted by a decimal library.
Which approach should you choose?
| Data | Recommended representation | Reason |
|---|---|---|
| Order, user, database, snowflake ID | JSON string | Identity is not arithmetic |
| Large integer used in calculations | String on the wire, BigInt in code | Exact integer arithmetic |
| Money or precise decimal | Minor-unit integer string or decimal string | Avoid binary floating-point rounding |
| Third-party JSON you cannot change | Lossless parser | Preserve original number tokens |
| Small counters and ordinary measurements | Number, after range validation | Simple and native when precision is sufficient |
How to detect risky numbers
After parsing, Number.isSafeInteger(value) can tell you whether a Number is safe for exact integer comparisons and arithmetic. It cannot recover lost digits. Validation is most useful at the API boundary, before an unsafe numeric token becomes a Number.
function assertSafeInteger(value, field) {
if (!Number.isSafeInteger(value)) {
throw new RangeError(`${field} is not a safe integer`);
}
} For an existing API, add contract tests containing values immediately below, at, and above Number.MAX_SAFE_INTEGER. Also test negative values, long decimals, and very large exponents if the domain permits them.
How JSONDock avoids silent rounding
JSONDock parses numbers with a lossless representation, preserving their source digits across formatting, repair, tree and table views, copying, and JSON-to-XML conversion. It also shows a “numbers preserved” status when a document contains values that would be unsafe as ordinary JavaScript numbers.
This does not change the rules of JavaScript in your application. It gives you a reliable inspection surface so the diagnostic tool does not modify the value while you are investigating it.