JSON vs YAML vs TOML: 使用哪种配置格式
三种最常见配置格式的实用比较和用例。
配置格式看起来可以互换——直到某个解析器悄悄把 no 变成 false、截断一个64位ID,或者从配置文件里实例化任意对象。JSON、YAML和TOML主导着现代工具链,团队经历的大部分痛苦都来自不清楚每种格式究竟在哪里会变形。这是一篇机制层面的比较,不是口味之争。
三份规范,三种哲学
JSON由RFC 8259和ECMA-404定义:六种值类型、没有注释、语法可以写在一张餐巾纸上。YAML 1.2.2(2021)是一份约一百页的文档,描述了三个表示层、标签、锚点和多文档流。TOML 1.0.0(2021)介于两者之间:一种带类型、注释友好的格式,明确设计为可以无歧义地映射到哈希表。
规范的体量在实践中很重要。所有主流JSON解析器的行为基本一致。而两个YAML解析器可以对同一个文件合理地得出不同结果,因为包括PyYAML在内的许多库仍实现YAML 1.1语义,另一些则实现1.2。
JSON: 可预测,但数字有锋利的边缘
{
"port": 8080,
"debug": true,
"hosts": ["a.example.com", "b.example.com"]
}
JSON真正的风险藏在数字和重复键里:
- 数字精度。 RFC 8259没有规定精度要求,但JavaScript把每个数字都解析为IEEE 754双精度浮点数。超过2^53 - 1的整数会静默丢失位数——这正是Twitter API同时提供
id和id_str的原因。64位标识符请序列化为字符串。 - 重复键。 规范只说键名应当(SHOULD)唯一。大多数解析器保留最后一次出现,有些保留第一次。当校验器和执行器以不同方式解析同一文档时,这种分歧曾被用于真实的安全绕过。
- 没有注释是刻意设计。 Douglas Crockford删除注释是为了防止基于注释的解析器指令。手工编辑的配置请使用JSONC(
tsconfig.json实际就是)或JSON5,并明确工具链接受哪一种——纯JSON解析器两者都会拒绝。
JSON适用于API载荷、锁文件,以及一切机器写入频率高于人类的场景。
YAML: 可读,但有一扇1.1形状的活板门
port: 8080
debug: true
hosts:
- a.example.com
- b.example.com
YAML 1.2把 no、yes、on、off 变成了普通字符串,但YAML 1.1解析器会把它们强制转换为布尔值——这就是著名的Norway问题:国家代码 NO 变成 false。1.1规则还会把 1:30 解析为六十进制整数90,把版本号 3.10 变成浮点数 3.1。由于你通常无法控制哪个解析器来读你的文件,运维规则是:给所有可能看起来像其他类型标量的字符串加引号。
还有两个机制值得敬畏:
- 锚点与别名(
&base/*base)能优雅地消除配置重复,但也让billion-laughs攻击成为可能:几个嵌套别名就能膨胀到数GB。解析不可信YAML时要设置别名限制,或者干脆拒绝别名。 - 任意对象构造。 PyYAML的旧版
yaml.load()可以从带标签的节点实例化任意Python类。永远使用safe_load或默认安全的解析器。
同时记住:缩进禁止使用制表符,一个多余的空格就能悄悄改变嵌套层级。如果生态强制你使用YAML(Kubernetes、GitHub Actions、Ansible),请搭配模式校验——yamllint加上schemastore.org的JSON Schema,能在CI阶段拦截大部分灾难。
TOML: 故意无聊
port = 8080
debug = true
hosts = ["a.example.com", "b.example.com"]
[database]
url = "postgres://localhost:5432/app"
[[server]]
name = "eu-1"
[[server]]
name = "us-1"
TOML的设计目标体现为具体的保证:
- 每个值都有明确类型:字符串、整数、浮点数、布尔值、数组、表,以及RFC 3339日期时间——日期是一等公民,不需要字符串约定。
- 重复键是硬性解析错误,而不是实现自由。
- 缩进没有语义:结构来自
[table]头和点号键,因此重新格式化永远不会改变含义。
代价是嵌套的工效学。深度嵌套的表数组很快变得冗长——这就是为什么Cargo和pyproject.toml用TOML很舒服,而Kubernetes清单不会。一个有用的启发式:如果配置嵌套超过三层,问题在于配置模式本身,而不是格式。
实践中的选择
- 机器间数据、API、锁文件: JSON
- 人工编辑的应用/工具配置: TOML
- 生态强制的基础设施配置: YAML,务必配合模式校验并给歧义标量加引号
- 需要注释但必须兼容JSON的配置: JSONC/JSON5,并明确写入文档
反复出现的错误
- 把64位ID作为JSON数字输出,几周后才调试数据损坏
- 不加引号的YAML值:国家代码、恰好全是数字的git SHA、
3.10这样的版本号 - 在没有别名/深度限制的情况下接受用户提交的YAML
- 假设所有解析器以相同方式处理重复键
- 用字符串拼接而非序列化器手工生成这些格式
格式之间的转换
三种格式在交集处共享同一数据模型(映射、数组、标量),因此机械转换通常只在一个方向上无损。JSON转YAML总是可行;YAML转JSON会在锚点、多文档和非字符串键上失败;TOML转JSON会丢掉注释层,而注释往往是文件里最有价值的内容。把人类编辑的格式当作唯一事实来源,机器格式在CI中生成,并且永远不要往返转换:只单向转换、两个产物都提交、在评审中对比差异。
提交前先验证:把配置粘贴到 sdk.is/json-formatter 的JSON格式化工具或 sdk.is/yaml-validator 的YAML校验器,让解析器先发现问题。