JSON1

格式

JSON 与 YAML

两者描述的结构完全一样:对象、数组、字符串、数字、布尔、null。真正有区别的地方是三件事:谁来写这个文件,能不能写注释,以及解析器会多激进地去猜你没加引号的那段文本是什么意思。

该用哪个

有用的分界线不在技术上,在作者是谁。JSON 是程序写、程序读的:生成、传输、解析,全程没有人参与。YAML 是人手写的,而且半年后还要再读一遍。

这一个问题就能定下大部分场景。HTTP 接口返回 JSON,换成 YAML 没有任何好处,客户端本来就都自带解析器。CI 流水线、Kubernetes 清单、应用配置用 YAML,因为它由人维护,需要留一句话说明超时为什么是 45 秒。

  • 机器之间传输 —— 用 JSON。更小,到处都能解析,没有歧义要处理。
  • 人手改的配置 —— 用 YAML。能写注释、标点少,这就是它存在的全部理由。
  • 要存储和查询的数据 —— 用 JSON。数据库能给它建索引,几乎没有数据库给 YAML 建索引。
  • 任何不可信来源 —— 用 JSON。它的语法极小,能出错的地方也就少得多。

真正的差别

YAML 1.2 是 JSON 的超集:任何合法的 JSON 也是合法的 YAML。反过来不成立,而这些缺口正是转换会丢东西的地方。

把文档在两种格式之间搬动时,会发生变化的行为。
JSONYAML
注释完全不允许# 到行尾
结构靠什么花括号和方括号缩进,也可以用括号
字符串引号必须加可选,坑就在这
尾随逗号报错不涉及
重复的键后者胜出,且不报错严格解析器会报错
多行字符串只能用 \n 转义|>
锚点复用没有&anchor*ref

JSON 转成 YAML

嵌套的数据能干净地转过去,而且结果确实更短 —— JSON 需要的那些标点,改由缩进承担了。数组和父级同一层缩进,这是通行写法,也是多数 linter 期望的样子。

注意 env 变成了什么:对象数组变成一串短横线,每个对象的第一个键跟短横线同行,其余的缩进在下面。Kubernetes 和 GitHub Actions 用的都是这个形状。

输入 JSON

{
  "service": "api",
  "replicas": 3,
  "ports": [8080, 8443],
  "env": [
    { "name": "LOG_LEVEL", "value": "debug" },
    { "name": "REGION", "value": "eu" }
  ],
  "limits": { "cpu": "500m", "memory": "512Mi" }
}

输出 YAML

service: api
replicas: 3
ports:
- 8080
- 8443
env:
- name: LOG_LEVEL
  value: debug
- name: REGION
  value: eu
limits:
  cpu: 500m
  memory: 512Mi
这是 /json-to-yaml/ 的真实输出。空对象和空数组会保留成 {}[],而不是变成你根本看不见的空行。

引号这个坑

这个坑真的会浪费时间,而且只在 YAML 这一侧咬人。在 JSON 里,"01234" 毫无疑问是字符串,因为字符串永远带引号。在 YAML 里引号是可选的 —— 于是解析器只能猜,而它是照着字符猜的。

从 JSON 转出去是安全的,因为工具知道自己拿到的是什么。任何会被重新读成数字或布尔的值,转出时都会补上引号:

输入 JSON

{
  "zip": "01234",
  "version": "1.10",
  "enabled": "yes"
}

输出 YAML

zip: "01234"
version: "1.10"
enabled: "yes"
这些引号不是装饰。去掉它们,下一个解析器读到的就是另外三个值。

手写 YAML 为什么会丢数据

还是这三个字段,改成手写、不加引号。这就是大家真正踩到的坑,而且没有任何提示:

  • 前导零 —— 邮编、手机号、账号都会丢。01234 变成 1234
  • 末尾零 —— 1.10 变成 1.1,版本号从此匹配不上。
  • `yes` 和 `no` —— 我们的解析器保留成字符串,与 YAML 1.2 一致。更老的 1.1 解析器(包括 PyYAML 的默认行为)会把 no 变成 false。这就是挪威问题:国家代码 NO 变成了布尔值。

手写的 YAML

zip: 01234
version: 1.10
enabled: yes

解析成 JSON

{
  "zip": 1234,
  "version": 1.1,
  "enabled": "yes"
}
前导零没了,1.10 变成了 1.1,邮编现在是个算术结果。三条都是 /yaml-to-json/ 的真实输出。

转一圈能剩下什么

JSON 转 YAML 再转回来是无损的,因为引号是工具替你写的。手写 YAML 转 JSON 再转回来就不是,而且损失发生在入口,早于任何转换。

所以安全习惯只有一条:YAML 里凡是可能被读成别的东西的字符串,都加引号。以文本形式存的数字是整个高危类别 —— 标识符、版本号、邮编、国家代码,以及任何带前导零的东西。

  • 注释两个方向都留不住。JSON 没地方放它,所以转换会丢掉每一行 #
  • 锚点和别名会被展开,不会保留 —— 复用变成了重复。
  • 键的顺序会保留,这件事对看 diff 的意义大于对正确性的意义。
  • JSON 转 YAML会替你给有歧义的字符串加引号,输出可以放心再解析一次。
  • YAML 转 JSON把手写的 YAML 贴进来,看看解析器实际读到了什么。
  • JSON 能写注释吗大家把配置搬去 YAML 的另一个原因,以及有哪些替代做法。
  • JSON 完全指南语法、类型,以及那些报错背后的规则。