YAML 与 JSON 与 TOML — 你应该真正使用哪种配置格式?
YAML、JSON 和 TOML 都用于存储配置,但它们是不可互换的。YAML 会静默地将国家代码转换为布尔值。JSON 不允许你添加注释。TOML 是团队中没人之前使用过的格式。以下是选择合适格式的方法。
2015年,一个Ansible的Playbook因为这一行而崩溃:
country: NO
配置加载时没有错误。没有解析器的抱怨。但 country 并未被设置为字符串 "NO"。它被设置为 false。因为在YAML 1.1中, NO 是布尔值。同样, yes, on, off, y,并且 n也是布尔值。这就是挪威问题,它多年来一直默默地破坏着配置文件。
这不是一个YAML的错误报告。这是一个引导你即将做出的决定:你的下一个配置文件使用YAML、JSON还是TOML?每种格式都有实际的权衡,而“只需使用生态系统中已有的格式”这一答案并不总是适用。
相同的配置,三种方式
在崩溃发生之前,以下是用这三种格式编写的相同应用程序配置:
YAML
# App configuration
app:
name: my-api
port: 8080
debug: false
database:
host: localhost
port: 5432
name: mydb
pool_size: 10
logging:
level: info
format: json
outputs:
- stdout
- /var/log/app.log
JSON
{
"app": {
"name": "my-api",
"port": 8080,
"debug": false
},
"database": {
"host": "localhost",
"port": 5432,
"name": "mydb",
"pool_size": 10
},
"logging": {
"level": "info",
"format": "json",
"outputs": [
"stdout",
"/var/log/app.log"
]
}
}
托米
# App configuration
[app]
name = "my-api"
port = 8080
debug = false
[database]
host = "localhost"
port = 5432
name = "mydb"
pool_size = 10
[logging]
level = "info"
format = "json"
outputs = ["stdout", "/var/log/app.log"]
YAML是最紧凑的,但语法负担最重。JSON是最冗长的,但最明确。TOML处于中间位置:无需YAML的隐式类型转换即可读取。
YAML:强大、宽容且充满陷阱
YAML是CI/CD流水线(GitHub Actions、GitLab CI、CircleCI)、Kubernetes清单、Ansible Playbook以及大多数开发工具的默认选择。你实际上并没有选择YAML——它是选择你的。
这些陷阱,已文档化:
1. 布尔值问题(挪威问题)
YAML 1.1——大多数解析器实际实现的规范——将大量字符串视为布尔值:
# YAML 1.1 boolean values (all parsed as true or false)
enabled: yes # true
disabled: no # false
active: on # true
paused: off # false
valid: true # true
invalid: false # false
# The Norway Problem in practice:
country_codes:
NO: Norway # Key "NO" is fine, but value "NO" becomes false
SE: Sweden
YES: Yemen # "YES" also becomes true
# The fix: quote your strings
country_codes:
NO: "Norway"
SE: "Sweden"
YAML 1.2(2009年发布)解决了这个问题——只有 true 且 false 是布尔值。问题是PyYAML直到2021年的6.0版本才完全采用1.2的行为,而Go的流行库 gopkg.in/yaml.v2 截至2024年仍使用1.1的语义。如果你使用Ruby的Psych版本小于4.0或任何早于6.0的PyYAML,你实际上在使用1.1版本。
2. 制表符将杀死你的配置
YAML禁止使用制表符进行缩进。只有空格是有效的。你的编辑器可能在显示制表符和空格时看起来相同,配置文件可能看起来正确,但YAML仍会抛出错误:
yaml.scanner.ScannerError: while scanning a block mapping
found character '\t' that cannot start any token
这是让初级开发者质疑自己职业选择的错误。请将你的编辑器配置为在YAML文件中将制表符扩展为空格。每个编辑器都支持此功能,但并非所有编辑器默认开启此功能。
3. 多行字符串并不直观
# | (literal block): preserves newlines exactly
description: |
Line one.
Line two.
Line three.
# Result: "Line one.\nLine two.\nLine three.\n"
# > (folded block): folds newlines into spaces
short_desc: >
This will all become
one long line.
# Result: "This will all become one long line.\n"
# Trailing newlines: | adds one, |+ adds all, |- strips them all
desc_stripped: |-
No trailing newline.
没有人能记住这一点而无需查阅资料。我使用的口诀是: | 看起来像换行符, > 看起来像被挤压在一起的内容。即使三年过去了,它仍然令人困惑。
当YAML胜出时
- 你正在编写Kubernetes清单、GitHub Actions工作流或Ansible Playbook——你别无选择。
- 你的配置包含大量解释非显而易见值的注释。YAML支持内联注释;JSON和TOML也支持,但YAML在注释密集的配置中感觉最为自然。
- 你的数据具有深度嵌套的结构,而TOML的扁平表形式会显得非常糟糕。
- 团队已经熟练掌握该格式,并且在流水线中已有校验工具(yamllint)。
JSON:枯燥但可靠的工具
JSON是为数据交换格式设计的,而不是配置格式。Douglas Crockford故意省略了注释——他的论点是,注释将被用于解析器会存在分歧的指令。因此 package.json 没有注释,而 tsconfig.json 是技术上带有注释的JSON(JSONC),大多数JSON解析器都不支持。
JSON在配置文件中的真正问题:
- 没有注释。 你无法解释为什么
"maxRetries": 3而不是5。你无法留下待办事项。你无法标记一个字段为已弃用。这对于生命周期超过作者的配置文件来说,确实非常痛苦。 - 没有尾随逗号。 向数组添加项目意味着必须修改上一行以添加逗号。每个JSON的差异都变成两行更改。每个合并冲突都比需要的更糟糕。
- 嵌套数据冗长。 六行的括号和方括号,而YAML只需三行缩进。
- 所有数字类型相同。 JSON不区分整数和浮点数。
1且1.0两者都是数字,而你的语言解析器将其解析为哪种类型取决于解析器本身。
但JSON的可预测性也是其主要特点。每个语言都有一个JSON解析器。规范明确无歧义。没有隐式类型转换。字符串始终是字符串—— "yes" 永远不会被静默地转换为 true。如果你需要在程序中验证JSON配置, IO Tools’ JSON 格式化器 可以在配置进入生产环境之前捕获语法错误——当有人手动编辑配置并忘记尾随逗号时,这非常有用。
当JSON胜出时
- API响应或被多个语言/服务消费的配置。JSON是通用的;TOML在某些生态系统中的支持是零散的。
- 你需要严格的类型保证。JSON Schema验证成熟、支持良好且被广泛使用(VS Code使用它来实现设置自动补全)。
- 配置是机器生成的。没有人会手动编写JSON——但机器可以轻松生成它。
- 你正在使用Node.js或前端JavaScript,其中JSON是原生公民。
TOML:为配置设计的有意见的格式
TOML(Tom的明显、最小语言)由GitHub联合创始人Tom Preston-Werner为配置文件专门创建。它于2021年1月达到v1.0版本。它是Rust的 Cargo.toml、Python的 pyproject.toml以及Hugo静态站点的默认格式。
TOML的设计理念:类型应明确,结构应尽可能扁平,且任何配置值应有唯一明显的方式书写。
# Types are unambiguous in TOML
name = "my-app" # string: always quoted
port = 8080 # integer
threshold = 3.14 # float
enabled = true # boolean: only true/false, no yes/no
created = 2024-01-15 # date: native type
tags = ["api", "prod"] # array
# "yes" is just a string. Always.
country = "NO" # string "NO", no boolean nonsense
粗糙的边缘:
- 表数组语法确实笨拙。
[[products]]且[products.details]看起来相似但行为完全不同。规范是合理的,但视觉上的区别并不明显。 - 深度嵌套变得冗长。 YAML用5行缩进完成的事情,TOML需要3个独立的节标题。对于深度超过3层的配置,TOML开始显得是错误的工具。
- 解析器的可用性。 TOML解析器存在于每个主要语言中,但它们在规范符合性上有所不同。 TOML符合性测试套件 定期揭示边缘情况。与之相比,JSON解析器的使用量高出数个数量级。
- 团队熟悉程度。 如果你在非Rust或Python生态系统中使用TOML,预计至少有一名团队成员会提交一个“这是什么格式?”的PR。
当TOML胜出时
- Rust项目——
Cargo.toml是标准,工具链非常优秀。 - Python项目使用
pyproject.toml(PEP 518)——这是目前推荐的工具配置位置,如Black、Ruff、mypy和pytest。 - 简单扁平的配置,其中YAML的缩进敏感性会成为劣势。
- 你希望拥有原生的日期/时间支持,而无需将其序列化为字符串。
快速决策指南
- Kubernetes / CI/CD流水线 / Ansible? YAML。没有选择。
- 被多个服务在不同语言中消费的API配置? JSON。
- Rust项目? TOML(Cargo.toml约定)。
- Python项目配置(linters、formatters、构建工具)? TOML(pyproject.toml是现在的标准)。
- 静态站点配置(Hugo、Zola)? TOML,尽管这些通常支持所有三种格式。
- Node.js项目配置? JSON(package.json生态系统),或如果你需要注释,则使用YAML。
- 人类会频繁编辑,并需要留下备注? YAML或TOML(两者都支持注释)。不是JSON。
- 你想要严格的类型安全和模式验证? JSON + JSON Schema。
大多数新项目诚实的答案:使用主要语言生态系统所期望的格式。Rust期望TOML。Python工具期望TOML或YAML。Node.js期望JSON。如果你编写的是语言无关的项目,TOML用于人类编辑的配置,JSON用于机器生成或消费的配置,是一个合理的默认划分。
