JSON 与 YAML 互转:配置文件格式的选择之道
什么是 YAML?
YAML 的全称是 “YAML Ain’t Markup Language”——对,你没看错,这个递归缩写本身就是一个程序员的幽默。跟 JSON 一样,YAML 是一种数据序列化格式。但它的设计哲学和 JSON 截然相反:JSON 为机器解析而生,YAML 为人眼阅读而造。
来看同一份数据,JSON 长这样:
{
"server": {
"host": "192.168.1.1",
"port": 8080,
"tls": true,
"routes": ["/api", "/admin", "/health"]
}
}
转成 YAML 之后:
server:
host: "192.168.1.1"
port: 8080
tls: true
routes:
- /api
- /admin
- /health
花括号、引号、逗号全消失了,层级关系靠缩进来表达。说实话,我第一次在 Docker Compose 里看到这种省略号、靠空格对齐的写法时,觉得这东西简直是在开倒车——直到某天需要手写一份 Kubernetes 的多容器部署配置,JSON 版本写到我怀疑人生,才懂了 YAML 的好。
YAML 的工作原理
YAML 的解析核心只有一句话:空白即语法,缩进即层级。
缩进就是一切
YAML 用缩进表示嵌套关系,和 Python 一样。规则简单到只有两条:同级缩进必须相同,子级比父级多缩进一层(通常是 2 或 4 个空格)。没了。
但你可能会想:那我混用空格和 Tab 会怎样?答案是解析器直接报错,不会给你任何侥幸。这是我当年踩的第一个坑——编辑器自动把空格转成了 Tab,一个 20 行的配置文件我排错了半小时。
标量类型自动推断
YAML 不需要你声明类型。解析器看到 8080 就知道是整数,看到 true 就知道是布尔值,看到 "192.168.1.1" 也不会把它当成什么奇怪的东西。这种”不写类型标签,靠上下文推断”的能力,叫标量自动解析。
有意思的是,YAML 支持 JSON 不支持的多行字符串:
description: |
这是第一行。
这是第二行。
空行和缩进都会被保留。
竖线 | 表示”保留换行”,右尖括号 > 表示”把换行折叠成空格”。写 CI/CD 配置里嵌脚本的时候,这两个操作符简直是救星。
YAML 转 JSON 的过程中发生了什么
把 YAML 转成 JSON 时,解析器做的事情其实很直接:
- 按缩进深度构建一棵树
- 缩进关系映射为 JSON 的嵌套对象/数组
- 标量值根据 YAML 的标签规则映射为 JSON 的 string / number / boolean / null
- 锚点和别名展开(后面会讲到)
- 输出纯净的 JSON
反之,JSON 转 YAML 是从严格的显式结构推导出最简的缩进表示——这个方向没有歧义,因为 JSON 的语法比 YAML 严格得多。
核心特性
| 特性 | 说明 |
|---|---|
| 缩进敏感 | 用空格表达层级,不能混用 Tab |
| 无分隔符 | 没有 {}、[]、, 和引号(绝大多数情况) |
| 注释支持 | 原生支持 # 注释,JSON 完全不支持 |
| 类型自动推断 | 整数、浮点、布尔、null 自动识别 |
| 锚点与别名 | 用 & 定义锚点、* 引用,减少重复 |
| 多文档 | 一个 YAML 文件可以包含多个文档,用 --- 分隔 |
注释这件事,单独拎出来说。JSON 的设计者 Douglas Crockford 是故意不加注释支持的——他认为注释会被滥用来存放解析指令,破坏数据格式的纯粹性。但从运维的角度看,一个没有注释的配置文件简直就是灾难。你打开一个 500 行的 JSON 配置,想知道某个字段是干什么的——对不起,只能去翻文档。YAML 的 # 注释解决了这个切肤之痛。
实际应用场景
1. Kubernetes 资源配置
Kubernetes 社区选 YAML 几乎是一个必然选择。一个典型的 Pod 定义里塞满了元数据、标签选择器、容器声明、挂载卷、环境变量——用 JSON 写的话,花括号能堆到七八层深,人眼根本数不过来谁嵌套了谁。YAML 的缩进结构天然匹配了”声明式资源描述”这个范式:
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80
同样的内容用 JSON 大概要 40 行,YAML 15 行就够了。
2. Docker Compose
多容器编排是 YAML 的天生主场。docker-compose.yml 里的 service、volume、network 三层嵌套用 YAML 表达出来一目了然,写起来和在脑子里画架构图的体验几乎一样。
3. Ansible Playbook
Ansible 选 YAML 有一个更深的原因:运维人员不是程序员。让一个运维工程师去手写 JSON 的自动化脚本——那个心理门槛和让一个网管去学编程差不多。YAML 的写法接近自然语言清单,学习曲线平缓得多。说白了,写 Ansible playbook 的感觉更像在填 Excel 表,而不是在写代码。
4. CI/CD 配置(GitHub Actions、GitLab CI)
这俩平台上 .yml 文件的标准格式都是 YAML。一个 GitHub Actions 工作流里有 on(触发器)、jobs(任务)、steps(步骤)三层结构,再加上每个 step 可能内嵌一段 shell 脚本,YAML 的多行字符串功能在这里体现得淋漓尽致。
常见误区与陷阱
误区一:YAML 就是”去掉花括号的 JSON”
这是初学者最容易犯的错误——觉得 YAML 就是 JSON 的简化版,可以无脑拷贝粘贴。实际上,YAML 的类型推断规则能在不经意间坑到你。
挪威问题(Norway Problem)
YAML 史上最著名的陷阱,没有之一。看这个例子:
countries:
- no # 本想写挪威的国家代码
- gb
解析器看到 no,自动推断为布尔值 false——因为在 YAML 1.1 规范中,no 是 false 的别名。这份配置的实际结果是 [false, "gb"]。你能想象一个新手遇到这种 bug 时的表情吗?“我没写 false 啊,怎么冒出来的?”
YAML 1.1 中会被误判的还有:yes/no/on/off → true/false,1.0.0 → 可能被当成字符串也可能被当成版本号(取决于解析器实现)。YAML 1.2 规范收敛了大部分这类问题,但市面上很多工具还在用旧版解析器。
解决方案很简单:不确定的时候,加引号。"no" 永远是字符串。
误区二:缩进随便对齐就行
我曾经写过一个 Ansible playbook,一个任务的 when 条件缩进多了一格——结果这个条件被解析为上层 block 的属性而非当前 task 的属性,整个执行逻辑完全跑偏。YAML 不会像编译器那样给你报”unexpected indent”,它默默地按自己的理解去跑,这才是最可怕的。
锚点与别名的”浅拷贝”陷阱
YAML 支持用 &anchor 定义锚点和 *anchor 引用它,减少重复配置:
defaults: &defaults
timeout: 30
retries: 3
service_a:
<<: *defaults
host: a.example.com
service_b:
<<: *defaults
host: b.example.com
好用归好用,但注意这是浅合并。如果锚点里有一个嵌套对象或数组,引用它的地方共享的其实是同一个引用。你在 service_a 里往那个数组 push 一个元素,service_b 里的数组也会多一个元素。这不叫 bug,这是设计如此——但知道的人不多。
YAML vs JSON vs TOML
| YAML | JSON | TOML | |
|---|---|---|---|
| 注释 | 支持 # | 不支持 | 支持 # |
| 可读性 | 高(人类优先设计) | 中(机器优先设计) | 高(INI 文件风格) |
| 缩进敏感 | 是 | 否 | 否 |
| 类型推断 | 激进(有时是陷阱) | 无推断 | 保守 |
| 规范复杂度 | 极高(2.4 万词规范) | 低(几页纸) | 中 |
| 主要场景 | K8s、CI/CD、运维自动化 | Web API、数据交换 | Rust/Cargo、Python pyproject.toml |
JSON 赢在简单和普适。它的规范只有几页纸,任何语言都有成熟的解析器,且解析速度极快。Web API 用 JSON 是历史选择也是理性选择。
TOML 走了一条中庸路线——比 JSON 可读性好,比 YAML 安全得多(没有类型推断的坑)。Rust 社区是 TOML 的主要推动力,Python 社区也逐步把 setup.py 迁移到了 pyproject.toml。
YAML 的优势领域非常明确:需要人频繁手写、需要注释、结构以嵌套为主、且数据流不经过网络传输的配置文件。一旦跨过”配置文件”这个边界到了”数据交换格式”,JSON 立刻扳回一城。
常见问题
Q: JSON 和 YAML 互转会丢信息吗?
结构信息不会丢,但注释会。JSON 不支持注释,所以 YAML 里的注释在转成 JSON 时会被丢弃。反过来 JSON 转 YAML 不会有这个问题——JSON 本来就没有注释。
Q: 为什么不用 YAML 做 Web API 的响应格式?
两个原因。一是解析慢——YAML 规范是 JSON 的几十倍复杂,解析器体积和耗时都远超 JSON。二是安全风险——YAML 的某些解析器支持”标签”机制,可以构造恶意 YAML 触发任意代码执行(类似 Python pickle 的反序列化漏洞)。JSON 的语法极简,攻击面天然就小。
Q: YAML 文件多大才不合适?
超过 2000 行的单一 YAML 文件就开始失控了——缩进太深导致视觉对齐困难,锚点引用混乱导致难以追踪数据流向。Kubernetes 社区的建议是把资源拆分成多个文件,用 Kustomize 或 Helm 模板来管理。
Q: “---”和“…” 在 YAML 里是干什么的?
--- 标记一个文档的开始,... 标记一个文档的结束。一个 YAML 文件里可以塞多个文档,像这样:
---
name: document1
---
name: document2
...
大部分日常场景用不到多文档,但在 Kubernetes 里,你可以把 Deployment 和 Service 放在同一个 .yml 里用 --- 分隔,然后一条 kubectl apply -f 全部部署。
Q: 我该选 JSON 还是 YAML?
如果你的数据是机器写给机器读的——Web API、消息队列、数据库存储——JSON 是正确答案。如果你的数据是人写给机器读的——配置文件、CI/CD 工作流、运维脚本——YAML 不会让你后悔。至于两个都需要的情况(比如微服务的配置中心),保留 JSON 作为内部序列化格式,对外暴露的时候转成 YAML 让运维团队去改,是实践中比较常见的折中方案。