TOML 与 YAML 与 JSON — 配置格式按它们会惹你生气的程度排序
每种配置格式最终都会背叛你。YAML 有缩进地狱和静默的布尔类型转换,JSON 有不支持注释的政策,TOML 有“等等,这是什么语法”的尴尬时刻。以下是每种格式带来的实际代价,以及何时该选择哪种格式。
每个项目最终都会让你选择一种配置格式。YAML无处不在。JSON比你的一些同事还早。TOML是最近出现的,它高举双手说:“其实,我就是为这个设计的。” 三者最终都会背叛你,只是方式不同。
这里是一个直接对比——相同的配置,三种格式——接着是每种格式让你后悔人生选择的具体时间点。
相同的配置,三种方式
一个基本的Web应用配置:名称、端口、调试标志、版本字符串、数据库设置、允许的来源。没有特别复杂的内容。正是在这里,格式差异开始显现。
托米
# App configuration
[app]
name = "my-app"
port = 3000
debug = false
version = "1.2.3"
allowed_origins = ["https://example.com", "https://api.example.com"]
[database]
host = "localhost"
port = 5432
name = "mydb"
YAML
# App configuration
app:
name: my-app
port: 3000
debug: false
version: "1.2.3"
allowed_origins:
- https://example.com
- https://api.example.com
database:
host: localhost
port: 5432
name: mydb
JSON
{
"app": {
"name": "my-app",
"port": 3000,
"debug": false,
"version": "1.2.3",
"allowed_origins": [
"https://example.com",
"https://api.example.com"
]
},
"database": {
"host": "localhost",
"port": 5432,
"name": "mydb"
}
}
一览无余
| 特征 | 托米 | YAML | JSON |
|---|---|---|---|
| 评论 | ✅ 是 | ✅ 是 | ❌ 否 |
| 类型推断 | 显式声明 | 激进(通常错误) | 显式声明 |
| 数组 | = ["a", "b"] | - item 或内联 | ["a", "b"] |
| 尾随逗号 | 不适用 | 不适用 | ❌ 非法 |
| 深层嵌套的配置 | 变得冗长 | 可读性尚可 | 冗长但明确 |
| 规范稳定性 | TOML 1.0(2021年,稳定版) | 1.1与1.2解析器混乱 | 稳定 |
| 空值支持 | ❌ 不允许空类型 | ✅ 是 (~ 或 null) | ✅ 是 (null) |
| 常用 | Cargo.toml,pyproject.toml | GitHub Actions,k8s,Docker | package.json,tsconfig.json |
YAML:在演示中看起来最棒,直到它不再如此
YAML在演示中看起来很棒。一个扁平的配置几乎像散文一样。问题出现在你遇到它的边缘情况时——而到那时,你的配置文件已经成了关键基础设施。
挪威问题
在YAML 1.1——大多数解析器默认使用的版本中——这些值都是布尔值: y, n, yes, no, on, off, true, false因此 country: NO 被解析为 country: false。这就是它被称为 挪威问题 的原因——挪威的国家代码是 NO。PyYAML在v6.0版本(2022年发布)中修复了这个问题。SnakeYAML(被许多Java工具使用)仍未完全解决。在使用配置值中的裸值之前,请检查你的解析器。 no 或 yes 在配置值中使用裸值。
类型推断错误
YAML中未加引号的值会被类型转换。 port: 8080 变成整数。 version: 1.10 变成浮点数 1.1 ——数学上相等,语义上错误。如果你忘记对版本字符串加引号,你可能会花十分钟去想为什么你的应用认为它运行在v1.1而不是v1.10。解决方法很无聊:对所有应该保持字符串的值加引号。但YAML不会强制你这样做,所以它不会。
缩进是关键
YAML中不允许使用制表符——不是建议,而是非法。在一个文件中混合使用两个空格和四个空格的缩进,你就会得到一个解析错误,而错误通常指向错误的行。GitHub Actions在这方面是最锋利的:一个缩进错误的 run: 块在运行时失败,而不是在解析时失败,因为工作流运行器只验证语法,不验证步骤结构。你将从CI作业中得到“意外值”错误,且没有明确指出是哪个步骤出错,你将花费20分钟添加调试输出,才意识到问题只是期望四个空格而用了两个空格。
如果你的YAML变得缩进不一致,那么 YAML格式化工具 会在你开始调试之前对其进行规范化。
TOML:真正考虑过配置格式的格式
Tom Preston-Werner(GitHub联合创始人)创建TOML,是因为他厌倦了写格式不一致的INI风格配置文件和让他感到意外的YAML配置文件。TOML 1.0于2021年1月发布,经过多年的修订。如今,它是Rust项目(Cargo.toml)、Python打包(pyproject.toml)和Hugo站点的标准。规范稳定,解析器一致,类型系统符合预期。
它做得正确的地方
- 没有意外的类型转换。
version = "1.10"始终是字符串。port = 3000始终是整数。你写的是什么,得到的就是什么。 - 注释工作方式正如你所期望的(
#到行尾),不像JSON。 - 扁平到中等嵌套的配置真正可读,不像深度嵌套的JSON。
表数组语法
TOML的主要障碍是它的表数组语法。如果你想有一个对象数组——比如多个数据库连接——语法如下:
[[databases]]
name = "primary"
host = "db1.example.com"
[[databases]]
name = "replica"
host = "db2.example.com"
每一列 [[double bracket]] section是数组中的一个项目。 databases 它有效。它明确无误。但每个第一次打开TOML文件的开发者都会问:“这是INI吗?”——因为它看起来有点像。当你要向从未见过TOML的贡献者进行入职培训时,这种不熟悉性会产生真实成本。
TOML还缺少 null 类型——有意为之。如果你的模式使用null表示“键存在但明确未设置”,你需要以不同方式建模(完全省略该键,或使用占位符值)。而深层嵌套的配置会变得冗长:TOML没有YAML的锚点/别名系统来重用子树,因此如果配置中有重复结构,就会出现大量复制粘贴。
这 TOML格式化工具 在你试图清理一个随着时间自然增长的TOML文件时非常有用。
JSON:你熟悉的魔鬼
JSON是为数据交换设计的——机器之间通信——而不是为人类编写配置文件。它最终成为配置格式,是因为每种语言都已有JSON解析器,这种便利性胜出。如今,JavaScript项目中大约有40个JSON配置文件,如package.json、tsconfig.json、.eslintrc.json,全部都是手动编辑的。
没有注释。仍然如此。
Douglas Crockford在2012年有意从JSON中移除了注释——他担心开发者会将其用作解析指令(类似于IE的条件注释)。自那以后,互联网每天都抱怨这一点。人们使用的变通方法:
- JSONC ——带有注释的JSON。VS Code用它来处理
settings.json且launch.json。无法被标准JSON解析器解析。非标准。 - JSON5 ——增加了注释、尾随逗号、未加引号的键、多行字符串。有规范和独立解析器。Babel用它来处理配置。仍然不是标准JSON。
- A
"_comment"钥匙 ——一个字符串字段,保存你的注释文本。有效。但看起来很荒谬。会进入你的数据模型。
尾随逗号
也非法。在数组或对象的最后项后添加一个尾随逗号, JSON.parse 会抛出 SyntaxError: Unexpected token } ——告诉你有问题,但不知道错误的逗号在何处。这是人类编写配置文件中最常见的JSON解析错误,发生是因为其他现代语言(JavaScript数组、Python列表、Rust枚举)都允许尾随逗号,而人类手动编写JSON时也带着相同的习惯。
JSON做得正确的地方
类型系统明确且通用。每个语言中的每个JSON解析器都对 true, 1, "1",并且 null 有统一的理解。JSON Schema是三种格式中最成熟的配置验证选项——VS Code用它来验证tsconfig.json和package.json,并提供行内错误高亮。当工具生成你的JSON(webpack、tsc、npm)时,你并不关心可读性——那是 JSON 格式化程序 的作用。
结论:根据上下文选择,而非偏好
使用JSON 当工具生成或消费它时(package.json、tsconfig、AWS配置、GitHub API响应),或当你需要JSON Schema验证时。不要手动编写超过必要的内容来对抗它。缺少注释确实是个问题,但其普及程度和工具支持是难以反驳的。
使用YAML 当配置主要由人类编写且相对扁平时——GitHub Actions工作流、Docker Compose文件、Kubernetes清单。对任何可能被误认为布尔值或数字的值(版本字符串、国家代码、以数字开头的值)都必须加引号。运行一个lint工具。永远不要使用制表符。将类型推断视为一个错误,而不是一个功能。
使用TOML 当你控制格式选择并希望避免意外的类型转换时。它是三者中最诚实的。如果你正在启动一个全新项目,且没有工具强制要求某种格式,TOML在六个月后最不可能让你感到意外。这种不熟悉性是一次性成本;明确性是永久性的。
