Your JSON Is Lying to You
Your JSON Is Lying to You
Mon Aug 03 2026
tutorials
javascript
json
serialization
... is lying to you
Run this in your browser console:
const payload = { id: 9007199254740993 }
console.log(JSON.stringify(payload))<br>// {"id":9007199254740992}
The value ends in 3, but the serialized result ends in 2. Nothing throws, and the output remains valid JSON, so the change is easy to miss when the value is an identifier buried inside a larger payload.
Now try a more varied object:
const original = {<br>id: 9007199254740993,<br>missing: undefined,<br>createdAt: new Date('2026-07-21T12:00:00Z'),<br>score: NaN
const copy = JSON.parse(JSON.stringify(original))<br>console.log(copy)<br>// {<br>// id: 9007199254740992,<br>// createdAt: "2026-07-21T12:00:00.000Z",<br>// score: null<br>// }
The number changed, the missing property disappeared, the Date became a string, and NaN became null. The object passed through the most familiar serialization round trip in JavaScript, yet the copy no longer carries the same data or types as the original.
How We Got Here
JSON emerged around 2001 as a lightweight way to exchange structured data between browsers and servers. Douglas Crockford named and popularised the format, and RFC 4627 formally specified it in 2006. The current standard is RFC 8259, published in 2017.
At the time, XML dominated data exchange on the web. JSON offered a much smaller grammar based on syntax JavaScript developers already recognised from object and array literals. A JSON document could be produced and consumed with little machinery, which made it a natural fit for the increasingly interactive web applications of the early 2000s.
That small grammar is still the source of JSON's appeal. It supports strings, numbers, booleans, null, arrays, and objects, giving different languages a common representation without importing one language's complete type system. Its design favours a minimal and portable wire format over exact preservation of every value available inside JavaScript.
JavaScript contains many values that fall outside that model. It has undefined, BigInt, symbols, special numeric values, objects with prototypes, and built-in collections with their own internal state. JSON.stringify must change or reject values the format cannot represent, while JSON.parse receives too little information to reconstruct most original types.
These differences become visible when a value leaves the process that created it. Writing it to a cache or database, sending it to another service, or reading it in another language can expose assumptions that remained hidden in JavaScript. The JSON can remain perfectly valid while the meaning changes on the way through.
This article examines those changes and explains how to define a wire representation that preserves the information your application actually needs. The goal is to make JSON boundaries explicit, so a convenient encoding does not quietly become an accidental data contract.
The Number May Be Wrong Before JSON Sees It
The opening example reveals the changed value during serialization, but the precision loss happens earlier. JavaScript rounds 9007199254740993 while evaluating the number literal, before JSON.stringify receives it.
const id = 9007199254740993
console.log(id)<br>// 9007199254740992
JavaScript stores ordinary numbers using the IEEE 754 binary64 format described by the ECMAScript Number type. This format can represent integers exactly from Number.MIN_SAFE_INTEGER through Number.MAX_SAFE_INTEGER, which is -(2^53 - 1) through 2^53 - 1. Beyond that range, adjacent integers can map to the same stored value.
JSON has a different contract: number grammar describes how a number is written as decimal text, but it does not prescribe one in-memory numeric type for every implementation. A system with a wider integer type can therefore produce valid JSON containing a value that JavaScript cannot store exactly.
const text = '{"id":9007199254740993}'<br>const parsed = JSON.parse(text)
console.log(parsed.id)<br>// 9007199254740992
Here the text still contains the exact integer. Precision is lost when JSON.parse converts that text into a JavaScript Number. If the parsed value is later serialized again, the rounded result becomes part of the outgoing data.
This matters for database identifiers, account numbers, invoice totals, and monetary values represented in minor units. A producer may send the correct digits while the receiving JavaScript program silently stores a different number. Validation performed after parsing cannot recover the original value because the distinction has already disappeared.
BigInt can hold integers beyond the safe Number range, but JSON has no corresponding value type. JavaScript rejects the conversion instead of choosing an encoding automatically.
const id = 9007199254740993n
console.log(id)<br>// 9007199254740993n
JSON.stringify({ id })<br>// TypeError: Do not know how to serialize a BigInt
Exact integers need an agreed...