YAML vs JSON vs TOML:何时使用哪种配置格式
每个项目最终都会遇到配置问题。你需要存储数据库凭证、定义部署环境、描述 CI 流水线,或者连接依赖项——某人必须选择一种格式。JSON?YAML?TOML?这个选择看起来随意,直到它咬了你一口。
Docker Compose 文件是 YAML。package.json 是 JSON。Rust 的依赖清单(Cargo.toml)是 TOML。Python 打包(pyproject.toml)是 TOML。GitHub Actions 工作流是 YAML。这些不是随机的——每种格式都因具体原因在其领域赢得了采用之战。理解这些原因让你能做出有意识的选择,而不是默认"模板用什么就用什么"。
本文是一份决策指南。读完之后,你将了解每种格式的真实权衡,看到导致生产事故的确切 YAML 陷阱,并拥有清晰的选择框架。
共同祖先:INI 文件
在深入研究现代竞争者之前,值得用三十秒了解一下 INI 文件——这三种格式在某种意义上都是对它的回应。
INI 文件长这样:
[database]
host = localhost
port = 5432
name = mydb
[server]
port = 8080
debug = true简单、人类可读、支持节、支持注释。问题同样简单:没有标准规范(每个解析器对边缘情况的解释不同),没有超过一层的嵌套结构,没有数组,没有显式类型。一切都是字符串,除非你的应用另行决定。
INI 在 1997 年用于 php.ini 很好。用于 2026 年描述 Kubernetes 部署就不行了。但它确定了正确的目标——人类可编辑性和注释——这些目标被现代格式继承或抛弃了。
JSON:数据的通用语言
JSON(JavaScript Object Notation)在 2000 年代初标准化,成为 API 的主流 wire 格式。它的优势来自它的约束。
JSON 做对的地方
JSON 有且只有六种值类型:字符串、数字、布尔值、null、数组和对象。规范无歧义。每个合规的 JSON 解析器都会以相同方式解析同一个文档。没有惊喜。
{
"database": {
"host": "localhost",
"port": 5432,
"ssl": true
},
"allowed_origins": ["https://example.com", "https://api.example.com"],
"timeout_ms": null
}port 的类型无歧义是数字。ssl 无歧义是布尔值。allowed_origins 无歧义是字符串数组。JSON 解析器无法误读这个。
JSON 也无处不在。每种语言都有标准库 JSON 解析器。浏览器原生解析它。PostgreSQL 等数据库有原生 JSON 列。它是不共享运行时的系统之间数据交换的最小公分母格式。
JSON 在配置方面做错的地方
JSON 没有注释语法。这是 Douglas Crockford(JSON 的创建者)的刻意设计决定,理由是数据格式中的注释会鼓励人们在其中放置解析指令,这会破坏格式的可移植性。
这对 API 响应来说是很好的原则。对配置文件来说是严重的问题。配置文件需要注释。你需要解释为什么 timeout_ms 是 30000 而不是 10000。你需要标记哪些字段可以安全修改。你需要留下一条注释说"在 staging 上不要把这个设置超过 100"。
变通方法都很糟糕。一些团队使用 "_comment" 字段。一些使用 JSON5(带注释的超集)并希望他们的工具能处理它。一些使用 JSONC(带注释的 JSON,由 VS Code 的 settings.json 推广)。这些都不是标准 JSON。
JSON 在配置上也很冗长。不允许尾随逗号,在重新排序字段时会导致不断的语法错误。每个字符串键都需要双引号。没有多行字符串语法——一个长 shell 命令必须转义到一行上。
JSON 属于: API 响应、服务间数据交换、主要由机器写入的包清单(package.json、composer.json),以及任何需要跨异构系统保证解析一致性的地方。
你可以在线验证 JSON 以在错误到达应用之前发现语法错误——这比你预期的更重要,因为单个缺失的逗号或多余的括号会完全破坏 JSON 解析器。
YAML:力量与危险
YAML(YAML Ain't Markup Language)被设计为对 XML 友好的人类替代方案。它富有表现力、可读性强,在 DevOps 工具中广泛使用。它也是常用配置格式中最危险的。
YAML 做对的地方
YAML 允许注释(#)。它从不带引号的值推断类型,所以你可以写 port: 5432 而不是 "port": 5432。它支持带有类 heredoc 语法的多行字符串。它很简洁:缩进取代了大括号和方括号。
database:
host: localhost
port: 5432
ssl: true
allowed_origins:
- https://example.com
- https://api.example.com
# 这是一个解释为什么超时时间高的注释
timeout_ms: 30000对于 CI/CD 配置——GitHub Actions、GitLab CI、CircleCI——YAML 带注释清晰表达嵌套结构的能力确实有用。GitHub Actions 工作流文件经常跨越数百行,带有嵌套条件、矩阵策略和环境变量块。JSON 会不可读。TOML 对深度嵌套结构会很笨拙。
YAML 的陷阱
YAML 的类型推断是出问题的地方。解析器查看不带引号的值并决定它们的类型。它做出的决定并不总是你所期望的。
挪威问题。 在 YAML 1.1(大多数解析器实现的版本——不是 YAML 1.2)中,裸 NO 被解析为布尔值 false。no、No、n、N、false、False、FALSE、off、Off 和 OFF 也一样。ISO 3166-1 国家代码列表包含挪威的 NO。带有国家代码列表的配置文件会悄悄地把挪威变成 false。
# YAML 1.1——解析器实际看到的
country: NO # → false(布尔值)
enabled: yes # → true(布尔值)
mode: on # → true(布尔值)
debug: off # → false(布尔值)解决方法是把所有应该是字符串的东西加引号:country: "NO"。但 YAML 的全部吸引力在于不必加引号。要求加引号来避免惊喜会削弱该格式的可读性优势。
八进制整数。 在 YAML 1.1 中,以 0 开头的数字被视为八进制。文件权限通常用八进制表示:chmod: 0755。YAML 解析器将 0755 读作十进制数 493,而不是 755。这已经破坏了真实的部署脚本。
# YAML 1.1——八进制解释
file_mode: 0755 # → 493(八进制 0755 的十进制等价)多行字符串歧义。 YAML 提供多种多行字符串语法——字面块(|)、折叠块(>)和内联。它们在换行符、尾随空白和缩进方面的行为不同。使用错误的会静默地产生错误的字符串。
# 字面块——保留换行符
command: |
npm install
npm run build
# 折叠块——换行符变成空格(适用于长文本)
description: >
这是一段长描述,将被
折叠成一行,用空格连接。制表符 vs 空格。 YAML 不允许用制表符缩进。静默插入制表符的编辑器会产生解析错误——或者更糟,根据制表位设置产生不同的解析结果。
YAML 1.2 修复了许多这些问题(包括布尔值列表),但大多数工具仍然实现 1.1。当你使用 YAML 时,你是在为你的工具链恰好带的解析器写代码,这可能与也可能不与你的假设相符。
TOML:专为配置设计
TOML(Tom's Obvious Minimal Language)由 GitHub 联合创始人 Tom Preston-Werner 于 2013 年创建,专门用作人类可以编写、机器可以无歧义解析的配置文件格式。
TOML 做对的地方
TOML 有明确、无歧义的类型。字符串总是带引号。数字总是数字。布尔值总是 true 或 false(小写,无别名)。没有从不带引号的值进行类型推断,所以没有挪威问题。
[database]
host = "localhost"
port = 5432
ssl = true
allowed_origins = [
"https://example.com",
"https://api.example.com",
]
# 这是一个解释为什么超时时间高的注释
timeout_ms = 30000TOML 对日期和时间有一级支持(publishedAt = 2026-04-04),而 JSON 和 YAML 将其留作应用程序自行解析的字符串。
节头([database]、[[servers]])明确且可读。点号键(database.host = "localhost")让你内联表达嵌套值。表数组([[servers]])给你清晰的对象列表语法。
[[servers]]
name = "production"
host = "prod.example.com"
port = 443
[[servers]]
name = "staging"
host = "staging.example.com"
port = 443TOML 规范精确且有版本控制。每个合规的 TOML 解析器对相同输入产生相同结果。
TOML 的不足之处
TOML 对浅到中等嵌套效果很好。对于深度嵌套结构——Kubernetes 清单或 Ansible playbook 中出现的那种——节头语法会变得重复。YAML 基于缩进的嵌套可以更自然地扩展到六七层深度。
TOML 的工具支持也不如 JSON 或 YAML。大多数语言有优秀的 TOML 库,但编辑器支持(语法高亮、验证、自动补全)不如 JSON 一致。
对比表
决策框架
一旦去掉营销语言,选择归结为三个问题。
这主要是机器生成的还是人类编写的?
如果文件由代码生成并由代码消费——API 响应、构建产物、序列化缓存条目——使用 JSON。它最可移植、最易解析,工具支持是通用的。不需要在没有人类阅读的文件中写注释。在通过网络传输之前压缩这些 JSON 文件可以有意义地减少有效载荷大小。
如果人类定期编写和编辑这个文件——项目配置、环境描述、工具清单——注释是不可协商的。这就排除了纯 JSON。
你控制工具链吗?
如果你被锁定在一个要求特定格式的生态系统中——Kubernetes 要求 YAML、GitHub Actions 要求 YAML、Cargo 要求 TOML、package.json 是 JSON——没有什么可决定的。使用生态系统期望的格式。
如果你从头开始设计一个新工具或新配置文件,你有真正的选择。
嵌套有多深,你的团队对细微 bug 的容忍度如何?
YAML 富有表现力但脆弱。仔细编写 YAML、为所有字符串加引号、运行 linter、理解 1.1 与 1.2 区别的团队可以安全使用它。快速推进、编辑器设置多样、不在 CI 中运行 YAML linting 的团队最终会在生产中遇到挪威问题或八进制 bug。
TOML 是在你控制格式时人类编写配置的更安全选择。显式类型意味着解析器错误在解析时就会出现,而不是在运行时——那时挪威已经变成了 false,你的功能标志逻辑悄悄地出错了。
快速参考
- API 响应或服务间数据交换: JSON
- 包清单(主要由机器写入): JSON
- Kubernetes、Docker Compose、GitHub Actions、Ansible: YAML(没得选)
- 你正在设计的新项目配置文件: TOML
- Rust 项目: TOML(
Cargo.toml) - Python 项目: TOML(
pyproject.toml) - 有简单键值需求的旧应用配置: INI(或迁移到 TOML)
验证配置文件
配置文件中的语法错误是最令人沮丧的 bug 之一——应用无法启动、错误消息晦涩难懂,而错误可能只是一个错位的字符。
对于 JSON,你可以在线验证并美化打印 JSON 以立即发现错误。在提交之前将 JSON 配置文件粘贴到验证器中需要五秒钟,可以防止一类 CI 失败。对于更深入的 JSON 工作流——为生产环境压缩、比较环境之间的配置变化——请参阅我们关于如何验证、压缩和美化打印 JSON 的指南。
对于 YAML,yamllint 是标准 CLI linter。如果你的团队编写生产 YAML,在 CI 中运行它不是可选的。对于 TOML,大多数语言生态系统都附带内置验证器(cargo check 隐式验证 Cargo.toml;python -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))" 直接验证任何 TOML 文件)。
格式是一个契约
无论你选择哪种格式,它都成为与项目中每位开发者和工具的契约。以后更改它会很痛苦——不是不可能,但很痛苦。Python 生态系统从 setup.cfg 和 setup.py 迁移到 pyproject.toml 花了数年时间,至今仍未完成。
有意识地选择。当机器是主要消费者且可移植性至关重要时用 JSON。当生态系统要求或深度嵌套结构需要可读时用 YAML。当人类编写文件、正确性重要、且你希望解析器在读取时捕获歧义而不是六个月后作为运行时 bug 出现时用 TOML。
配置格式不仅仅是一种文件格式。它是关于谁编辑文件、什么工具处理它,以及你对隐式类型强制转换信任程度的声明。有意识地做这个声明。
现在需要验证一个 JSON 配置文件?试试 FileCrank JSON Viewer——粘贴你的 JSON,立即发现错误,并美化打印以提高可读性。