JSON.parse Silently Corrupts Large Integers—and the Reviver Can't Save You
Problem Scenario
One day after going live, operations reported: "After opening the user detail page, the ID is wrong when navigating to another page; every click leads to a different person."
Investigation revealed that the user ID returned by the backend looked like this:
{
"userId": 19028374651234567891,
"name": "张三"
}
After the frontend parsed it with JSON.parse, the value of userId became:
19028374651234567000
The last few digits had completely changed. Using this wrong ID for requests during navigation naturally fetched a different person.
The most insidious part is—this bug is not consistently reproducible. It only triggers when the ID exceeds a certain number of digits; small IDs work perfectly fine, making it impossible to detect in the development environment.
Cause Analysis
JavaScript's Number Limit
All numbers in JavaScript follow the IEEE 754 double-precision floating-point standard (64-bit). The range of integers it can safely represent is:
- (2⁵³ - 1) ~ 2⁵³ - 1
i.e., -9007199254740991 ~ 9007199254740991
For integers beyond this range, when there are too many digits, JS performs rounding:
console.log(19028374651234567891);
// Output: 19028374651234567000
// ↑ The end is truncated, precision is lost
You can verify this using Number.isSafeInteger():
Number.isSafeInteger(19028374651234567891); // false
The Silent Loss of JSON.parse
The problem is that JSON.parse does not throw an error—it silently returns a number with lost precision, without any warning:
const data = JSON.parse('{"id": 19028374651234567891}');
console.log(data.id); // 19028374651234567000
// Completely silent, no exception, no console.warn
This is why this bug is particularly hard to diagnose: no errors, no warnings, just incorrect data.
Whose Fault Is It?
| Party | Argument | Analysis |
|---|---|---|
| Backend | "The database clearly stores the complete ID" | ✅ Data source is correct |
| Frontend | "The JSON string I received is also complete" | ✅ Transport layer is correct |
| Problem | Occurs at the JSON.parse parsing stage | 💥 This is where it breaks |
No one is to blame; it's the natural chasm between the JSON specification and the JS Number type.
Solutions
Solution 1: Backend Returns Strings (Recommended)
The backend uniformly passes large integers to the frontend as strings:
{
"userId": "19028374651234567891",
"name": "张三"
}
The frontend is unaware and uses it directly as a string. However, the API documentation must clearly mark which fields are string-type IDs to avoid mixing types.
Solution 2: Backend Uses a Lossless JSON Library (Backend Side)
If changing types on the backend is inconvenient, use a serialization library that supports lossless numbers:
- Java: Use
Jackson+@JsonSerialize(using = ToStringSerializer.class) - Go: Use the
json:"userId,string"tag - Node.js: Use the
lossless-jsonlibrary
Solution 3: Frontend Handles It with a Reviver (Frontend Side)
JSON.parse accepts a second argument, reviver, which can process specific fields during parsing:
function safeParse(json) {
return JSON.parse(json, (key, value) => {
// Preserve specified large number fields as strings
if (key === 'userId' || key === 'id') {
return String(value);
}
return value;
});
}
const data = safeParse('{"userId": 19028374651234567891}');
console.log(data.userId); // "19028374651234567891" ✅
But this method has a fatal flaw: when the reviver executes, the number has already been truncated. So if the number in the JSON string itself exceeds the safe range, the reviver receives an already incorrect value, and String(value) cannot save it.
// The reviver receives a value that has already been truncated
JSON.parse('{"id": 19028374651234567891}', (key, val) => {
console.log(val); // 19028374651234567000 ❌ Precision already lost
return String(val); // "19028374651234567000" ❌ Wrong
});
Therefore, Solution 3 is only usable when the backend already returns strings, limiting its practical use.
Solution 4: Use BigInt + Custom JSON Parsing
For scenarios where the frontend genuinely needs to parse large integers, the json-bigint library can be used:
import JSONbig from 'json-bigint';
const data = JSONbig.parse('{"id": 19028374651234567891}');
console.log(data.id.toString()); // "19028374651234567891" ✅
The principle is to convert large integers to the BigInt type during parsing.
Solution 5: Use proto3's int64 Type at the HTTP Level (Ultimate Solution)
For gRPC / protobuf projects, proto3's int64 is converted to a string by default in JS environments, naturally avoiding this pitfall.
Identification Methods in Real Projects
How to quickly know if your project has this hidden risk?
// Run this in the console
function checkSafeRange() {
const testVal = 9007199254740993;
console.log('Original value:', testVal);
console.log('Is safe:', Number.isSafeInteger(testVal));
console.log('After parsing:', JSON.parse(String(testVal)));
}
// More practical: scan all ID fields
fetch('/api/user/1')
.then(r => r.json())
.then(data => {
Object.entries(data).forEach(([key, val]) => {
if (typeof val === 'number') {
console.log(key, Number.isSafeInteger(val) ? '✅' : '❌ Precision risk', val);
}
});
});
Key Takeaways
- JS safe integer range is
±2⁵³-1; exceeding it loses precision - JSON.parse loses precision silently—no errors, no warnings, extremely hard to diagnose
- Recommended solution: Backend changes large numeric IDs to string format for return
reviveris useless—it receives a value that has already been truncated- Node.js/Go/Java backends all have corresponding serialization solutions to avoid this problem
- Detect early: Use
Number.isSafeIntegerto assert all numeric IDs during the development phase
One-line summary: Extra-long numbers in JSON silently lose precision during
JSON.parse. The easiest fix is for the backend to replace all numbers exceeding 16 digits with strings. Don't letJSON.parseeat your IDs.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
I've learned something.