{ } JSONDock 中文

JavaScript · JSON precision

Why does JavaScript lose precision when parsing large integers in JSON?

A valid JSON number can be larger or more precise than a JavaScript Number can safely represent. The failure is especially dangerous because JSON.parse often succeeds—and silently changes the digits.

precision-demo.js
const source = '{"orderId":900719925474099312345}'; const data = JSON.parse(source); console.log(data.orderId); // 900719925474099300000
THE PARSE SUCCEEDS

But the original ID is gone.

1JSON syntax is valid
2The value becomes a Number
3The nearest representable value is used
4No exception warns you

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.

The practical dangerThe parser does not throw. A modified identifier can travel through logs, caches, UI state, requests, and database calls before anyone notices.

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?

DataRecommended representationReason
Order, user, database, snowflake IDJSON stringIdentity is not arithmetic
Large integer used in calculationsString on the wire, BigInt in codeExact integer arithmetic
Money or precise decimalMinor-unit integer string or decimal stringAvoid binary floating-point rounding
Third-party JSON you cannot changeLossless parserPreserve original number tokens
Small counters and ordinary measurementsNumber, after range validationSimple 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.