Your JSON Is Lying to You

Your JSON Is Lying to You / 你的 JSON 正在欺骗你

Your JSON Is Lying to You 你的 JSON 正在欺骗你

Run this in your browser console: 在你的浏览器控制台中运行以下代码:

const payload = { id: 9007199254740993 }
console.log(JSON.stringify(payload)) // {"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. 该数值以 3 结尾,但序列化后的结果却以 2 结尾。程序没有抛出任何错误,输出依然是合法的 JSON,因此当该值作为标识符隐藏在更大的数据负载中时,这种变化很容易被忽略。

Now try a more varied object: 现在尝试一个更复杂的对象:

const original = { 
  id: 9007199254740993, 
  missing: undefined, 
  createdAt: new Date('2026-07-21T12:00:00Z'), 
  score: NaN 
}
const copy = JSON.parse(JSON.stringify(original))
console.log(copy)
// {
//   id: 9007199254740992,
//   createdAt: "2026-07-21T12:00:00.000Z",
//   score: null
// }

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. 数字变了,缺失的属性消失了,日期变成了字符串,NaN 变成了 null。该对象经历了 JavaScript 中最常见的序列化往返过程,但副本却不再携带与原始对象相同的数据或类型。

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. JSON 大约在 2001 年出现,作为一种在浏览器和服务器之间交换结构化数据的轻量级方式。Douglas Crockford 命名并推广了这种格式,RFC 4627 在 2006 年对其进行了正式规范。目前的标准是 2017 年发布的 RFC 8259。

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. 当时,XML 在 Web 数据交换中占据主导地位。JSON 提供了一种更小的语法,其基础是 JavaScript 开发者已经熟悉的字面量对象和数组语法。生成和解析 JSON 文档几乎不需要复杂的机制,这使其非常适合 21 世纪初日益交互化的 Web 应用。

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. 这种精简的语法至今仍是 JSON 的魅力所在。它支持字符串、数字、布尔值、null、数组和对象,为不同语言提供了一种通用的表示方式,而无需引入某种语言完整的类型系统。其设计倾向于一种极简且可移植的传输格式,而不是精确保留 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. JavaScript 包含许多超出该模型的值。例如 undefined、BigInt、Symbol、特殊的数值、带有原型的对象以及具有内部状态的内置集合。JSON.stringify 必须更改或拒绝该格式无法表示的值,而 JSON.parse 收到的信息太少,无法重构大多数原始类型。

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. 当一个值离开创建它的进程时,这些差异就会显现出来。将其写入缓存或数据库、发送给另一个服务,或用另一种语言读取它,都可能暴露出在 JavaScript 中被隐藏的假设。JSON 本身可能完全合法,但其含义却在传输过程中发生了改变。

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. 本文将探讨这些变化,并解释如何定义一种能够保留应用程序实际所需信息的传输表示方式。其目标是明确 JSON 的边界,从而避免一种便捷的编码方式悄然变成意外的数据契约。

The Number May Be Wrong Before JSON Sees It / 数字在被 JSON 处理前可能就已经错了

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. 开篇的例子揭示了序列化过程中的数值变化,但精度损失发生得更早。JavaScript 在评估数字字面量时,即在 JSON.stringify 接收到它之前,就已经对 9007199254740993 进行了舍入。

const id = 9007199254740993
console.log(id) // 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. JavaScript 使用 ECMAScript Number 类型所描述的 IEEE 754 binary64 格式存储普通数字。该格式可以精确表示从 Number.MIN_SAFE_INTEGER 到 Number.MAX_SAFE_INTEGER 之间的整数,即 -(2^53 - 1) 到 2^53 - 1。超出此范围,相邻的整数可能会映射到同一个存储值。

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. JSON 有一套不同的契约:数字语法描述了数字如何以十进制文本形式书写,但它并未规定每种实现必须使用哪种内存中数值类型。因此,一个拥有更大整数类型的系统可以生成合法的 JSON,其中包含 JavaScript 无法精确存储的值。

const text = '{"id":9007199254740993}'
const parsed = JSON.parse(text)
console.log(parsed.id) // 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. 在这里,文本中仍然包含精确的整数。当 JSON.parse 将该文本转换为 JavaScript Number 时,精度就会丢失。如果解析后的值随后再次被序列化,舍入后的结果就会成为输出数据的一部分。这对数据库标识符、账号、发票总额以及以最小单位表示的货币值来说至关重要。

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. 发送方可能发送了正确的数字,而接收方的 JavaScript 程序却静默地存储了一个不同的数字。解析后进行的验证无法恢复原始值,因为差异已经消失了。

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. BigInt 可以存储超出安全 Number 范围的整数,但 JSON 没有对应的数值类型。JavaScript 会拒绝转换,而不是自动选择一种编码方式。

const id = 9007199254740993n
console.log(id) // 9007199254740993n
JSON.stringify({ id }) // TypeError: Do not know how to serialize a BigInt

Exact integers need an agreed representation before they cross the wire. A decimal string is often the simplest choice because JSON implementations can preserve its digits exactly. 精确的整数在传输前需要一种约定的表示方式。十进制字符串通常是最简单的选择,因为 JSON 实现可以精确保留其数字。

const json = JSON.stringify({ id: '9007199254740993' })
const parsed = JSON.parse(json)
console.log(parsed.id) // "9007199254740993"

The schema must state that id is a decimal string rather than an ordinary number. The receiving program can keep it as an identifier or convert it to BigInt when arithmetic is required. Other encodings can work as well, provided that the producer and consumer agree on them explicitly. 模式(Schema)必须声明 id 是一个十进制字符串,而不是普通数字。接收程序可以将其保留为标识符,或者在需要进行算术运算时将其转换为 BigInt。只要发送方和接收方明确达成一致,其他编码方式同样可行。

undefined Can Mean Multiple Things / undefined 可能意味着多种含义

JavaScript distinguishes between a missing property and a property whose value is undefined. Both produce undefined when read directly, but property checks reveal the difference: JavaScript 区分了“缺失的属性”和“值为 undefined 的属性”。直接读取时两者都会产生 undefined,但属性检查可以揭示其中的区别:

const value = { present: undefined }
console.log(value.present) // undefined
console.log(value.missing) // undefined
console.log('present' in value) // true
console.log('missing' in value) // false

That distinction can be relevant in application code. A missing property may mean that no… 这种区别在应用程序代码中可能非常重要。缺失的属性可能意味着没有……