YAML 解析陷阱:那些让你踩坑的缩进和类型推断
什么是 YAML?
YAML 是一种面向人类阅读和编写的数据序列化语言。它的设计信条很简单:配置文件是给人看的,不是给机器看的。YAML 是 JSON 的严格超集——任何合法的 JSON 都是合法的 YAML,但反过来不行,YAML 多了很多 JSON 没有的花活。
用缩进表示层级、用冒号分隔键值、用短横线表示列表——乍一看比 JSON 干净得多。没有花括号,没有引号,没有逗号。干净是真的干净,但坑也是真的多。
我第一次认真写 YAML 是在配 GitHub Actions 的时候。看着文档照猫画虎写了个 workflow,提交上去 CI 直接红了,报错信息含糊得一批。反复检查了半小时,最后发现是多写了一个空格——缩进层级错了一格。从那之后我就对 YAML 的缩进有了肌肉记忆般的恐惧。
YAML 的工作原理
YAML 解析器的核心逻辑分两步:先把文本切成 token(缩进、标量、映射、序列),再根据缩进深度构建树状结构。
缩进就是结构
YAML 用空格(不是 Tab,Tab 在 YAML 里是非法字符)的缩进来表示层级关系:
parent:
child1: value1
child2:
grandchild: value2
解析器数空格,2 格缩进代表子级,4 格代表孙级。这套逻辑翻译成 JSON 就是:
{
"parent": {
"child1": "value1",
"child2": {
"grandchild": "value2"
}
}
}
隐式类型推断——YAML 最恐怖的特性
YAML 跟 JSON 最大、也是最危险的区别在于:它会自动猜你的数据类型。JSON 里 "true" 是字符串,true 是布尔值,泾渭分明。YAML 里你写 true,解析器自动给你一个布尔值;写 123,给你一个整数;写 2023-01-15,给你一个日期对象。
听起来挺贴心的?往下看你就知道这有多致命了。
核心特性
| 特性 | 说明 |
|---|---|
| 人类友好 | 无括号、无引号、无逗号,缩进即结构 |
| 注释支持 | 用 # 写注释,JSON 不支持 |
| 引用和锚点 | 用 & 定义锚点,* 引用,避免重复定义 |
| 多行字符串 | ` |
| 隐式类型 | 自动推断布尔值、整数、浮点数、日期、null |
| JSON 超集 | 任何合法 JSON 都可用 YAML 解析器正常解析 |
注释是我觉得 YAML 对 JSON 唯一的碾压级优势。你没法在一个 .json 配置文件里解释某个字段为什么是这个值——JSON 里写注释是语法错误。而 YAML 里随手加个 # 这里选 8080 是因为 80 端口被 nginx 占了,过三个月你回来看自己配的配置文件也不会一头雾水。
实际应用场景
1. CI/CD 流水线配置
GitHub Actions、GitLab CI、CircleCI 全都用 YAML 定义 workflow。我之前写 CI/CD pipeline 时,在一个步骤里把环境变量的值写成了 NO——打算表示”不启用某个特性”。结果 YAML 1.1 解析器把 NO 自动推断成了布尔值 false,程序读到环境变量的值是 false 而不是字符串 "NO",行为完全跑偏。这就是著名的 “Norway 问题”。
2. Docker Compose 和 Kubernetes
Docker Compose 的 docker-compose.yml、Kubernetes 的各种资源清单,全是 YAML。K8s 的 deployment 配置动辄上百行,嵌套四五层,缩进稍有差池整个 apply 就直接报错。
有一次我在配 Kubernetes deployment,把环境变量里的 version: 1.10 写进 YAML,K8s 的应用读到的是 1.1——因为 YAML 把 1.10 当成了浮点数,直接把末尾的零给抹了。字符串当数字解析,小数点后的精度直接蒸发了,你能不崩溃?
3. Ansible 自动化
Ansible 的 playbook 和 inventory 文件都基于 YAML。用 | 操作符写多行 shell 脚本片段特别方便——相比 JSON 里那堆 \n 转义,YAML 的写法确实清晰一个数量级。
4. 各类框架配置文件
Ruby on Rails 的数据库配置、Jekyll 的前置元数据、Home Assistant 的智能家居自动化规则——全是 YAML。说白了,凡是开发者需要手写的配置,大概率是 YAML。
常见误区与陷阱
陷阱一:挪威问题的幽灵——隐式类型推断
这是一个写入计算机民俗的经典 bug。在 YAML 1.1 规范中,以下字符串都会被自动转成布尔值:
true, True, TRUE → true
false, False, FALSE → false
yes, Yes, YES → true
no, No, NO → false
on, On, ON → true
off, Off, OFF → false
所以如果你有一个国家列表配置文件:
countries:
- US
- UK
- NO # 挪威的 ISO 代码,被解析成了 false!
挪威直接被 YAML 从地图上抹掉了。解决方法很简单但很烦:不确定类型的值一律加引号 "NO" 或 'NO'。我们这个工具的 YAML 转 JSON 功能底层使用了 YAML 1.2 规范的解析器,但默认会用引号包裹所有标量值来规避这个问题——你可以关掉这个安全策略,但我建议你别关。
陷阱二:多行字符串的 | 和 >
YAML 提供了两种多行字符串操作符,很多新手分不清。
|(literal block scalar):保留换行符。你写几行,输出的字符串里就有几个\n。>(folded block scalar):把多行折叠为一行,换行符变成空格。有多余空行才会保留换行。
script: |
npm install
npm run build
npm test
这个会保留为三行命令。而:
description: >
这是一段很长的描述文字,
它在 YAML 里换行了,
但解析后会变成一行。
解析结果是 这是一段很长的描述文字,它在 YAML 里换行了,但解析后会变成一行。。我的经验是:shell 脚本用 |,长篇描述用 >,不确定就自己在脑子里跑一遍解析器的逻辑再决定。
陷阱三:六十进制数
YAML 1.1 支持六十进制(sexagesimal)数,用冒号分隔——看起来就跟时间一样:
duration: 1:30:00
这会被解析成整数 5400(1×3600 + 30×60 + 0)。如果你的配置里恰好有类似时间格式的字符串,比如视频时长的标签 length: 2:15,那你会拿到整数 135 而不是字符串 "2:15"。更坑的是——1:2 不会触发六十进制解析(因为不够三部分),但 1:02:00 会。YAML 2.0 规范里这个特性已经被移除了,但很多旧解析器还在用 1.1 规范。
陷阱四:缩进不可见
YAML 的缩进是语义的一部分,但它用空格——人类肉眼分辨 2 个空格和 4 个空格的能力远不如分辨 { 和 }。一个缩进错误可能完全改变数据的层级结构,而且不像 JSON 那样会报语法错误——YAML 解析器会默默地给你一个结构完全不同的结果。
YAML vs JSON vs TOML
| YAML | JSON | TOML | |
|---|---|---|---|
| 人类可读性 | 极高,但有陷阱 | 中,括号多 | 高 |
| 注释 | 支持 | 不支持 | 支持 |
| 类型推断 | 有(危险) | 无 | 有(较安全) |
| 缩进敏感性 | 极高 | 无 | 低 |
| 工具链成熟度 | 高 | 极高 | 中 |
| 主要用途 | CI/CD、K8s、配置 | Web API、数据交换 | 配置文件(Rust/Python) |
JSON 是机器对机器的协议,没有注释、没有推断、没有歧义——它呆板,但呆板在这种场景下是优点。YAML 是人写配置的选择,灵活但陷阱多。TOML 像是两者的折中——有注释、缩进不敏感、类型推断相对克制,Rust 和 Python 生态里用得越来越多。
我个人现在对配置文件的偏好是:API 交互绝对是 JSON,手写配置优先 TOML,只有项目生态强依赖 YAML(比如 K8s、Ansible)的时候才用 YAML。被 YAML 坑了太多次之后,你会对它的”便捷”产生一种斯德哥尔摩综合征般的复杂感情。
常见问题
Q: YAML 到 JSON 的转换安全吗?
转换本身安全,真正不安全的是你 YAML 源文件里的隐式类型。一个 "NO" 字符串在 YAML 里可能已经被解析成了布尔值,再转 JSON 就变成 false 而不是 "NO"。所以转换之前,把你 YAML 里所有不该被推断类型的值都用引号括起来。我们这个工具提供了”引号包裹所有字符串”的选项,建议开着。
Q: YAML 里用 2 空格还是 4 空格缩进?
2 空格是 YAML 社区的约定俗成,绝大多数官方示例都用 2 格。你非要用 4 格也行,但在同一个文件里绝对不能混用——解析器会用缩进差值算层级,混用等于自掘坟墓。
Q: 怎么把 JSON 转回 YAML?
反向转换同样有坑。JSON 的 null 转 YAML 会是 null(字符串),但 YAML 里 ~ 和空值也代表 null。如果你想干净地转换,我们有 JSON 转 YAML 工具,它会处理好这些映射细节。
Q: YAML 的锚点和引用转了 JSON 会怎样?
锚点 & 和引用 * 在转换为 JSON 时会被展开为实际值。JSON 不支持锚点语法,所以引用会被原地替换。如果你 YAML 文件里用锚点是为了减少重复,转 JSON 后文件会变大——这是设计使然,不是 bug。
Q: 为什么我的 CI/CD 配置在本地能跑、push 上去就报错?
十有八九是缩进问题。GitHub Actions 等 CI 平台的 YAML 解析器和本地的可能不同——版本、实现、严格程度都有差异。建议在 push 之前用在线 YAML 校验工具或 IDE 插件做一次语法检查,比事后对着红色 build log 抓狂好得多。