10 分钟搞懂 JSON

JSON 是一个很小的数据格式,标点规则严格,边界情况的代价却出人意料地高。搞清楚它能表示的六种值,在系统边界上做校验,别再假装日期和超大整数是 JSON 的原生类型。

🎙️ 发布并录制于: ·

01JSON 只有六种值类型

JSON 能装的东西只有对象、数组、字符串、数字、布尔值和 null。整个类型系统就这些。没有日期,没有字节数组,没有集合,没有注释,没有 undefined,也没有专门的整数类型。对象把字符串键映射到值,数组保留顺序。对象里键的先后顺序永远不该承载含义,哪怕很多 parser 恰好会保留它。

{
  "name": "Mina",
  "score": 9.5,
  "active": true,
  "middle_name": null,
  "skills": ["SQL", "Python"],
  "address": { "city": "Leeds" }
}
null 不等于缺失

{"middle_name": null} 是明确给出了这个键,值为空。{} 是根本没有这个键。很多 API 用这个区别来表达「清空这个字段」和「保持原样」。含义要写进你的接口约定,别让每个客户端自己猜。

02语法严格是故意的

键和字符串必须用双引号。成员之间用逗号分隔,但结尾多一个逗号是非法的。注释也是非法的。数字不能是 NaN 或 Infinity。JSON 是一种传输格式,不是给人天天手写的配置语言。如果这个文件每天都有人改,就用 TOML 或 YAML,再配上校验,别自己发明一种带注释的 JSON。

// Invalid JSON
{ 'port': 3000, "debug": true, }

// Valid JSON
{ "port": 3000, "debug": true }
Unexpected token 怎么修

Chrome 可能报 SyntaxError: Unexpected token ' in JSON at position 2第一步:看报错位置上的那个字符。第二步:把单引号的键和字符串换成双引号。第三步:删掉结尾逗号和注释。第四步:跑一下 python -m json.tool settings.json。成功时它打印格式化后的 JSON,失败时给出准确的行号和列号。

03嵌套是为了归属,不是为了炫技

子对象真的属于父对象时,嵌套才有意义。收货地址就该放在订单快照里面。五十层泛泛的 dataattributesitems 不会让 API 更灵活,只会逼每个调用方都写一堆防御代码。稳定的标识符放在靠上的位置,只有当重复出现的值真的有顺序时才用数组。

{
  "order_id": "ord_204",
  "customer_id": "cus_18",
  "shipping_address": {
    "line1": "14 King Street", "city": "Leeds"
  },
  "items": [
    { "sku": "BK-7", "quantity": 2 }
  ]
}

约定允许字段缺失时,访问嵌套数据要写得防御一点。在 JavaScript 里,order.shipping_address?.city ?? "unknown" 能兜住对象不存在的情况。但这不能替代一份写清楚的结构说明。把示例和 schema 发布出来,客户端才知道哪些键是必填的。

04输入做解析,输出做序列化

解析是把 JSON 文本变成语言里的值,serialization 是反过来。这条边界要一直看得见。JavaScript 用 JSON.parseJSON.stringify。Python 里字符串用 json.loads,文件用 json.load,输出用对应的 dump 函数。永远不要靠拼字符串来生成 JSON,转义早晚会背叛你。

// JavaScript
const user = JSON.parse('{"name":"Mina","active":true}');
const text = JSON.stringify(user, null, 2);

# Python
import json
user = json.loads('{"name":"Mina","active":true}')
text = json.dumps(user, indent=2, ensure_ascii=False)
两个真实的序列化故障

遇到 {'name':'Mina'},Python 会报 json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)第一步:确认输入是 JSON,而不是打印出来的 Python 字典。第二步:在生产方改引号,别在这边做无脑字符串替换。第三步:python -m json.tool 验一遍。对象自己指回自己时,JavaScript 会报 TypeError: Converting circular structure to JSON第一步:看报错里点出的那个环。第二步:显式构造一个普通对象再序列化,或者把这条反向引用换成一个 ID。第三步:除非丢掉这个字段本来就写进了约定,否则别用 replacer 把环藏起来。

05用 JSON Schema 校验结构

合法的 JSON 也可能是没用的数据。数量写成负数、邮箱那个键拼错了,parser 都会收下,因为两种写法语法上都合法。JSON Schema 描述的是必填字段、类型、格式、取值范围,以及允不允许出现未知属性。每一条信任边界上都要校验。进程内部就用正常的类型化模型,别对同一个对象反复校验个没完。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["sku", "quantity"],
  "properties": {
    "sku": { "type": "string", "minLength": 1 },
    "quantity": { "type": "integer", "minimum": 1 }
  },
  "additionalProperties": false
}

对小的请求对象,我的默认选择是 additionalProperties: false。悄无声息的拼写错误,比一个明确的 400 响应更糟。对外的带版本 API 则要兼容地加字段,同时让客户端容忍自己不认识的字段。这两个目标是冲突的,所以这个开关要想清楚再定,别照抄别人的 schema。

06流和日志用 JSONL

一份普通的 JSON 文档是一个完整的值。上百万条记录的数组通常必须整体解析,想往后追加也很别扭。JSON Lines,也叫 JSONL 或 NDJSON,每行存一个完整的 JSON 值。它更适合日志、导出、pipeline 和增量处理。它不是一份合法的普通 JSON 文档,所以格式要老实标出来。

# users.jsonl
{"id":1,"name":"Mina"}
{"id":2,"name":"Luis"}
{"id":3,"name":"Asha"}

# Process one record at a time
with open("users.jsonl", encoding="utf-8") as rows:
    for line in rows:
        user = json.loads(line)
按访问方式选格式

必须完整的设置对象或 API 响应用 .json。记录彼此独立,而你想追加、流式读、切分,或者遇到一行坏数据还能接着往下恢复,就用 .jsonl。别给 JSONL 的行加逗号,也别套上方括号。那样它就变回一个数组,流式处理的好处也没了。

07HTTP 上传 JSON 需要一份明确约定

发 JSON 时带上 Content-Type: application/json。服务端能返回多种格式时,用 Accept: application/json。解析响应体之前先看 HTTP 状态码,因为中间的代理可能返回一个 HTML 错误页。解析成功不代表业务成功,状态码和响应字段仍然算数。

curl --fail-with-body https://api.example.com/orders \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data '{"sku":"BK-7","quantity":2}'

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{"order_id":"ord_204","status":"accepted"}
HTML 冒充 JSON

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON 一般说明响应其实是 HTML。第一步:把状态码和收到的 Content-Type 打出来。第二步:看原始响应体。第三步:修掉产生这个 HTML 页面的 URL、认证或代理问题。第四步:只有响应约定说是 JSON 时才去解析 JSON。别把那个尖括号切掉再喂给 parser。

08大整数和日期需要约定

JSON 的数字没有承诺位宽,但 JavaScript 的数字超过九千万亿之后就会丢整数精度。标识符不参与算术,所以大 ID 要用字符串发。JSON 也没有日期类型。发一个清楚的 ISO 8601 时间戳,带上时区,最好是以 Z 结尾的 UTC。像生日这种只有日期的值,就保持成只有日期的字符串。

{
  "order_id": "9223372036854775807",
  "created_at": "2026-07-25T14:05:00Z",
  "birthday": "1994-03-12",
  "amount_minor": 1299,
  "currency": "GBP"
}

涉及钱的时候,如果这个货币有惯用的最小单位,就发整数的最小单位加上货币代码。需要任意小数精度时,发十进制字符串,并且把小数位数写进文档。绝对不要发 2026-07-25 09:00 这种不带偏移量的本地时间。两个客户端可以把它解析成两个不同的时刻,而且都觉得自己是对的。

09把 JSON 当成不可信输入

解析 JSON 比执行代码安全,但解析出来的数据仍然由发送方控制。限制请求体大小和嵌套深度,校验 schema,对请求的动作做鉴权,并且在用到值的地方按目标环境做转义。JSON 挡不住 SQL 注入、路径穿越、XSS,也挡不住 prototype pollution。

// Good boundary order
limitBody("256kb");
const value = JSON.parse(rawText);
validateSchema(value);
authorize(request.user, value.order_id);
await db.query("SELECT * FROM orders WHERE id = $1", [value.order_id]);

// Never do this
eval("(" + rawText + ")");
Object.assign(globalDefaults, value);
parser 不是防火墙

超大的请求体要在缓冲之前就拒掉。合并对象之前要拒掉预期之外的键,JavaScript 系统里尤其是 __proto__constructorprototype。数据库查询一律参数化。渲染到 HTML 时对输出做转义。写日志前把密码、令牌和个人信息脱敏。该问的问题不是「JSON.parse 收下了吗」,而是「这个值在这里被允许吗」。

10JSON 速查表

把这份速查表放在负责边界的那段代码旁边。

# six values
object  {}    array  []    string  "text"
number  12.5  boolean true false  null null

# strict rules
double quotes · no trailing comma · no comments · UTF-8
no undefined · no NaN or Infinity · keys are strings

# JavaScript
JSON.parse(text)            JSON.stringify(value, null, 2)

# Python
json.loads(text)            json.dumps(value, indent=2)
python -m json.tool file.json

# HTTP and modeling
Content-Type: application/json
large ID → string           timestamp → ISO 8601 with zone
stream of records → JSONL   untrusted input → limit + validate

我的看法很简单:JSON 在边界上应该是无聊的。ID 用字符串,时间戳写明确,进来的数据配 schema,记录流用 JSONL。如果你的格式需要注释、引用、自定义日期字面量,再加五套解码约定,那它已经不是一份简单的 JSON 约定了。换一个格式,承认数据本来是什么样子。

Tell me what missed

A correction is more useful than a compliment. This goes straight to the person who writes SwiftGrasp.

Was this page useful?
0/1000

Please do not include passwords, private keys, or personal information.