YAML vs JSON vs TOML:各自的优势、使用场景与 YAML 的隐式类型陷阱

各格式的主场领域

每种数据序列化格式都在自己的领域称霸。JSON(JavaScript 对象表示法,标准化为 ECMA-404 与 RFC 8259)是 HTTP API 的通用语言:几乎每个 REST 端点都使用它,每种编程语言都有零依赖的 JSON 解析器,其语法只需一页即可描述完整。YAML(YAML Ain't Markup Language)主导 DevOps 工具的配置文件——Kubernetes 清单、GitHub Actions 工作流、Ansible 剧本和 Docker Compose 文件全都采用 YAML。其多行字符串和注释支持使它远比 JSON 更适合人类日常编辑的配置文件。TOML(Tom's Obvious, Minimal Language)是项目级配置的首选:Rust 包的 Cargo.toml、Python 构建元数据的 pyproject.toml 和 Hugo 网站配置均使用 TOML。其强类型字面量和 [section] 语法让配置文件易于阅读,且不会有静默的打字错误。

这些领域的划分并非偶然——它们反映了各格式的设计优先级。JSON 重视互通性和确定性;YAML 重视人类可读性和表达力;TOML 重视类型安全和可预测的解析。理解这些优先级有助于选择正确的工具,避免将一种格式的假设带入另一种格式的问题空间。

YAML 的隐式类型转换:挪威问题

YAML 最臭名昭著的陷阱是其激进的隐式类型强制转换。在 YAML 1.1(大多数解析器直到近期仍使用的版本,包括 PyYAML < 6.0 和 Ruby 的 Psych 4.0 之前)中,未加引号的标量会被一系列正则表达式测试以确定其类型。布尔测试会匹配 yesnoonofftruefalse——且不区分大小写。这在使用 ISO 3166-1 alpha-2 国家代码的软件中造成了实际的数据完整性错误:挪威的国家代码是 NO,YAML 1.1 会静默地将其转换为布尔值 false。同样的问题也会影响 fi(芬兰,在某些解析器中解析为 false),以及任何值恰好是未加引号的 yesno 的配置键。像 norway: NO 这样的映射在运行时会变成 {norway: false}——没有错误,没有警告。

YAML 1.2(2009 年发布,由 ruamel.yaml 和较新的解析器采用)将布尔集合收紧为只有 truefalse(区分大小写),消除了 yes/no/on/off 变体。然而,数百万个生产环境文件和读取它们的工具仍在 YAML 1.1 解析器上运行。安全规则:始终为可能被误读的字符串值加引号——国家代码、标志型词语、看起来像数字的字符串。使用 'NO' 而非 NO,使用 '1e2' 而非 1e2(后者会成为浮点数 100.0),使用 '2024-01-01' 而非 2024-01-01(某些解析器会将其转换为日期对象)。YAML 规范本身也承认这种歧义是各实现之间已知的错误来源。

其他 YAML 陷阱:制表符、锚点与文档分隔符

缩进是 YAML 的语法——而制表符在 YAML 缩进中从来无效。规范明确禁止在缩进位置使用制表符。然而制表符在大多数编辑器中默认是不可见的,而一个意外的制表符会产生指向错误行号的扫描器错误。解决方法是在编辑器中启用「显示空白字符」,并配置在 .yaml 文件中将制表符展开为空格。相关的陷阱是混合块和流式风格:key: {a: 1, b: 2} 是有效的(流式映射作为值),但缩进流式风格的内容很微妙——流式序列中的换行符会重置列计数器,视觉上看起来正确的内容可能解析方式不同。

YAML 锚点&)和别名*)允许在文档中重复使用一个节点,这对于包含重复容器规格的长 Kubernetes 清单非常有用。但它们也能让未限制别名深度的解析器受到「十亿笑声」式的扩展攻击:一个有 9 层别名、每层扩展 10 个引用的文档会产生超过十亿个节点。大多数生产环境解析器(PyYAML 5.1+、使用 SafeLoader 的 snakeyaml)会限制别名深度;始终使用安全加载 API。--- 文档分隔符允许在一个文件中包含多个 YAML 文档——kubectl apply -f 依赖于此——但某些解析器在未明确迭代的情况下只返回第一个文档,静默丢弃其余部分。

TOML 的显式类型与日期时间优势

TOML 在语法层面为每个值分配类型,不进行隐式强制转换。整数无需引号(port = 8080),浮点数需要小数点(timeout = 1.5),布尔值为小写的 truefalse,字符串始终需要引号。这意味着产生未加引号单词的打字错误是解析错误,而非静默的类型变更——解析器会拒绝加载文件,而非继续处理错误数据。TOML 1.0(2021 年发布)也规定了四种作为第一类字面量的日期时间类型:offset_date_time1979-05-27T07:32:00Z)、local_date_timelocal_datelocal_time。没有其他主要序列化格式将日期视为内建类型——JSON 没有;YAML 1.1 有日期类型,但其解析在各实现间不一致。

[table][[array of tables]] 语法清晰地映射到嵌套对象和对象数组,这是 TOML 对 YAML 缩进块结构的回答。一个 [[servers]] 块后接另一个 [[servers]] 块会向 servers 数组追加第二个元素——明确、可读,且不会因一个多余的空格而破坏。其权衡是:TOML 不支持锚点或多文档文件,因此具有重复子树的深层配置层次结构比其 YAML 等效形式更冗长。对于一次编写、频繁读取的项目元数据文件,这种冗长通常不是问题。

JSON 的严格性作为特性

JSON 的语法没有可选功能、没有类型强制转换、没有文档级指令。在任何平台上,符合规范的解析器读取 {"active": true} 都会产生一个布尔值——不是字符串 "true",不是整数 1。这种确定性使 JSON 成为 API 的默认传输格式,尽管它不如 YAML 对人类友好。RFC 8259(取代了 RFC 7159 和 RFC 4627)进一步收紧了规范:JSON 文本是一个序列化值——单个对象、数组、字符串、数字、布尔值或 null。对象中的重复键被明确标记为「不应」(实现可能接受它们,但结果未定义),编码必须是无 BOM 的 UTF-8。

缺少注释是 JSON 最常被提及的抱怨。存在解决方法:JSON5(带有注释和尾部逗号的超集)、JSONC(VS Code 的 settings.json 使用)以及在解析前剥离 // 行。对于人类和机器都需要读取的配置文件,这些注释扩展有所帮助——但它们不是标准 JSON,许多解析器会拒绝它们。惯用的解决方案是使用 TOML 或 YAML 进行编写,仅将 JSON 用作机器交换格式,由构建步骤生成。这正是 package.json 工具生态系统使用的模式:人类通过 .eslintrc.yaml 或带有 JSONC 的 tsconfig.json 进行配置,工具输出 JSON。

选择正确的格式

实际的决策规则很简单:JSON 用于任何跨越服务边界的数据(HTTP API、消息队列、由代码生成的配置);YAML 用于运维人员手动编辑且需要注释或多行字符串的配置文件——尤其是在强制使用它的生态系统中(Kubernetes、GitHub Actions);TOML 用于强类型和扁平文件结构比 YAML 表达力更重要的项目级配置。如有疑问,优先使用您的生态系统已经在使用的格式——对抗惯例的代价远超其价值。

如果您继承了一个大型 YAML 配置文件并怀疑存在隐式类型错误,快速审查方法是通过 YAML 1.2 代码检查器(yamllintspectral)运行它,并搜索符合布尔值或数字模式的未加引号值。对于新项目,考虑添加 Schema(JSON Schema 通过工具适用于三种格式)——无论您选择哪种格式,它都能在类型错误到达生产环境之前捕获它们。TeaFun 的 YAML / JSON / TOML 转换器让您在浏览器中于三者之间切换,无需安装即可发现结构差异。