YAML 何时会改变含义:类型、缩进与 Properties 转换
YAML 1.0 在 2001 年由 Clark Evans、Ingy döt Net 与 Oren Ben-Kiki 发布,目标是“对人友好”——XML 对机器友好、JSON 当时还不存在。愿景很美:一份“读起来像散文”的配置文件。现实是 YAML 1.1 规范里那张转换表造成的故障,比任何其他配置格式的任何单一特性都多——而 1.2 修了大部分,但你的工具大概率仍然在跑 1.1。
YAML 与 Java 的 .properties 都是为了一件事——把配置写成文本——但走了相反路线。YAML 把表达力开到最大(嵌套结构、anchor、多行字符串、tagged 类型)。.properties 把无聊开到最大(一键、一值、一行)。世界用了二十年才发现:那个无聊的、对的次数比任何人愿意承认的都多。
YAML 一段话讲完
YAML 是 JSON 的超集——任何合法 JSON 都是合法 YAML 1.2。在 JSON 文法之上 YAML 加了:基于缩进的嵌套、不带引号的字符串、多行字符串、注释(#)、anchor / alias 引用(&id / *id)、显式类型 tag(!!str、!!binary),以及一长串"未经请求就把字符串变成数字、布尔、日期"的隐式类型转换。
格式读起来像精简的配置 DSL。空文件合法。单个字符串也是合法文档。列表、map、scalar 像乐高一样组合。
service: api-gateway
replicas: 3
flags:
- logging
- tracing
env:
LOG_LEVEL: info
TIMEOUT: 30s
这是 YAML 的表面。麻烦在底下。
Norway Problem
这个 bug 定义了 YAML 1.1 的名声。
countries:
- GB
- IE
- NO
- SE
YAML 1.1 下这份文档解析成:
{'countries': ['GB', 'IE', False, 'SE']}
NO 在 YAML 1.1 里不是国家代码,是布尔 false。同样还有 No、no、N、n、false、False、FALSE、off、Off、OFF。布尔转换表接受的输入范围荒谬地宽。挪威的两字母国家代码恰好在里面。Stack Overflow 问题、库 bug 报告、至少一起广为报道的部署事故,根因都同一个:YAML 1.1 以为 NO 是布尔。
YAML 1.2 在 2009 年修了——布尔严格只 true 与 false。但是你工具里很多解析器仍然默认 1.1 语义——多年里 PyYAML 默认 loader、若干 Ruby 库、若干构建系统的 YAML 都是。修复办法是用 1.2 严格 loader(PyYAML 的 safe_load + Loader=yaml.CSafeLoader,或 ruamel.yaml),但你得先知道要去要求。
其他隐式转换枪口
Norway Problem 只是最有名的。YAML 1.1 还会这么转:
12:34:56当成 60 进制数:12*3600 + 34*60 + 56 = 45296。时间字符串变成整数。0123当成八进制83。任何前导 0 都可能被当八进制。1.0是浮点,1.0e2是 100.0。版本字符串写成1.0变成浮点丢掉零。yes、no、on、off是布尔。null、Null、NULL、~、(空) 都是 null。2010-04-01是日期对象。版本号长得像日期就变日期。
模式:任何看起来像某种特殊类型的值,就被当成那个类型——除非加引号。安全的规则因此是:任何可能被误解的值,全部加引号。version: "1.0"、code: "NO"、time: "12:34:56"、port: "0123"。代价是轻微视觉噪声;收益是没有意外类型转换。
YAML 1.2 显著缩小了隐式类型集:只有 true/false、null/~/空、整数、浮点会被转。NO、yes、12:34:56、2010-04-01 都是字符串。但你的解析器在 1.1 模式下,这帮不了你。
缩进:以最糟的方式有意义
Python 的有意义缩进基本无碍,因为 Python 解析器立刻告诉你缩进错了。YAML 的有意义缩进危险的地方在于:很多缩进选择都"合法但含义不同"。
items:
- name: alpha
qty: 3
- name: beta
qty: 5
vs
items:
- name: alpha
qty: 3
- name: beta
qty: 5
第二份也是合法 YAML,但 qty 现在是 items 的兄弟,而不是每个 item 的属性。解析器不会告诉你写错了——它只是给你一份不同形状的文档,然后你应用在下游炸开。
YAML 还禁止 tab 缩进,多数编辑器自动 tab→空格能默默修;偶尔也会默默混用,那种情况下解析器爆出晦涩报错。
务实规则:部署前用 lint 工具校验(yamllint,Kubernetes 用 kubeval)。格式不给你编译期检查,你必须自己加上。
anchor 与 alias:你会后悔用的特性
YAML 允许在文档别处引用一个节点:
defaults: &defaults
timeout: 30
retries: 3
services:
api:
<<: *defaults
port: 8080
worker:
<<: *defaults
port: 9000
&defaults 声明 anchor,*defaults 引用,<<: 把 anchor 的 map 合进当前 map。看起来 DRY,规模上还行,会以三种方式失败:
- 和多文档流的组合奇怪。 anchor 是文档内的;跨文档传递需要扩展支持。
- dump 出去的内容不能安全地往返于任意工具。 多数工具读时解 alias,dump 出来失去结构。
- 被攻击者写的 YAML 可以做 DoS —— YAML 版的 billion laughs:
a: &a [1,1,1,1,1,1,1,1,1]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
# ... 到 g 你已经在分配 10⁹ 个 int
多数现代解析器对 alias 展开有上限。别把上限关掉。别接受不可信源的带 anchor 的 YAML,除非解析器明确安全处理。
多文档流
同一份 YAML 文件可以含多份文档,用 --- 分隔:
---
kind: Deployment
metadata:
name: api
---
kind: Service
metadata:
name: api-svc
Kubernetes 与 Helm 大量使用。这是 YAML 规范的一部分;多数库返回一个序列(Python yaml.safe_load_all、Go yaml.NewDecoder().Decode 循环),可选的尾部 ... 文档结束标记很少见但合法。
多行字符串:比你想要的口味更多
YAML 有五种写跨行字符串的方式,空白处理各异:
literal: | # 保留换行
line one
line two
folded: > # 把换行折成空格
line one
line two
literal-strip: |- # 保留换行,去掉末尾换行
line one
literal-keep: |+ # 保留换行,**保留所有**末尾换行
line one
double-quoted: "line one\nline two\n" # JSON 风格转义可用
| 字面块保留换行;> 折叠块用空格连接(空行变成真换行)。chomping 指示符 - 去掉末尾换行,+ 全部保留,默认保留恰好一个。chomping 写错,"YAML 里的 shell 脚本"会多一个换行直接打断脚本。
.properties:那个不死的无聊格式
Java 的 .properties 早于千禧年,几乎没变。文法:
key=value
key.with.dots=另一个值
key.with.spaces = 等号两侧有空格 OK
multi.line=第一行\
第二行延续
empty.value=
# 注释行
! 也是注释行
key:value # 冒号也作分隔
差不多就这些。键是字符串;值是字符串;不存在嵌套;用点分键伪装层次。官方编码是 ISO-8859-1,其他字符用 \uXXXX 转义(Java Properties 文件是凝固在 UTF-8 之前 i18n 的化石)。
为什么它活下来:几乎不可能解析错。没有隐式转换。没有布尔 Norway。没有 anchor。没有缩进可错。解析器在任何语言里二十行代码就够。Spring、Java EE、Maven、Gradle、log4j、Kafka、Hadoop、JBoss——整个 JVM 生态都标准化在它上面,因为它简单而且保持简单。
缺点:原生没嵌套、原生没列表、值同行不能写注释、默认 ISO-8859-1。现代 Spring 与 Spring Boot 同时接受 .properties 与 YAML;多数团队新代码选 YAML,旧代码留 .properties。
互转
概念上:
.properties的a.b.c=value→ YAML{a: {b: {c: value}}}。a.b[0]=x(Spring 的列表扩展)→{a: {b: [x]}}。- YAML 的 map 展平成点分键;YAML 的列表展平成索引键(
a[0]、a[1])。
两个陷阱:
- 两边都丢类型。 YAML 的
enabled: true是布尔 true。.properties等价是enabled=true,那是字符串"true"。消费者得知道何时去转。 .properties允许键名里含点而不是结构性。web.url=http://...既可能是"叫 web.url 的键",也可能是"web 的 url 子项"。YAML 转换得选一边,正确选择因应用而异。
转换大体机械,本站工具按标准展平规则双向实现。它无法还原源里就没法表达的信息——.properties 没法编码带 anchor 引用的 YAML——但对"扁平配置 vs 嵌套配置"这种常见情形,往返干净。
常见坑
- YAML 1.1 的布尔 把
NO、Yes、Off当布尔。引起来或升级解析器。 - 前导 0 在 1.1 里被当八进制。给电话号码、邮编、账号引号——那些"看起来是数字字符串、但不是数字"的东西。
12:34当 60 进制。 时间引起来。- tab vs 空格缩进。 YAML 拒绝 tab。
- 缩进层级错 静默地解析成不同形状文档。lint。
- 不带引号的日期 变成日期对象。
2024-06这种版本字符串有风险。 null、~、空值 都是 null。空key:是 null,不是空字符串。要空字符串写key: ""。- 多行块的 chomping 让你多了一个不想要的换行。下游对空白敏感时用
|-。 .properties的行延续 行末\要求下一行去掉前导空白;某些手写解析器搞错。.properties的 Unicode:ISO-8859-1 文件里忘记\uXXXX转义非 ASCII。现代 Spring 你告诉它就接受 UTF-8;查你的 loader。
哪个用哪个
- YAML 用于:Kubernetes 清单、GitHub Actions workflow、Ansible playbook、生态期望 YAML 的地方、人类要写多层配置的地方。
- .properties 用于:Spring 配置、Java application properties、消费者是 JVM 且结构扁平。
- TOML 当你有自由选择。它就是 YAML 想成为的样子:人可读、有注释、没有隐式类型转换、没有有意义缩进。Cargo、
pyproject.toml、许多新工具用它。 - JSON 用于一切机器对机器,包括工具生成的配置。
- 环境变量 用于最小、最扁平、最与传输无关的配置。十二要素应用对这点是对的。
如果你能选,新人类编写的配置选 TOML。如果生态替你选了(Kubernetes、GitHub Actions、Spring),按它的来:字符串防御性加引号、CI 里 lint、永远不要把国家代码扔给 YAML 1.1。
主要参考资料
用于核对本文技术细节的标准与官方文档。
在 YAML 与 .properties 之间互转
本站工具在浏览器内做 YAML / .properties 双向转换,按结构化规则展平 / 还原。Spring 配置与 Kubernetes / GitHub Actions YAML 互迁时很有用。数据不会离开浏览器。
打开 YAML / .properties 工具相关文章
继续阅读同一主题领域的实践指南。
Node 生产 Dockerfile 里到底该有什么,不该有什么
网上大多数 Node Dockerfile 都把 node_modules 直接拷进镜像、用 root 运行、最后产出一个 900 MB 的层。本文只讲那几个真正影响构建时间、镜像体积和运行时安全的决定:基础镜像、多阶段构建、依赖层缓存、NODE_ENV 陷阱,以及为什么你的 docker-compose 不该照搬生产。
在用户之前发现缺失的翻译键和插值参数不匹配
缺失的翻译键会把原始键路径直接渲染给用户,插值参数不匹配会渲染出空串或崩溃。这两者在评审中都容易漏,因为开发者的语言包永远有全部键。本文讲如何结构化比较 locale JSON 文件、找出缺失键,并在发布前抓住参数不匹配。
能真正压测 UI 的 mock 数据(而不是只把页面填满)
大多数 mock 数据是同一行复制十遍、只换个 id。它填满页面,却什么都测不到。本文讲如何生成能压测布局边界、长名字、缺失字段、空状态,以及会破坏格式化代码的日期和数字格式的 mock 数据,并通过字段推断让一个 JSON 样本一步变成贴近真实的数据集。