← 返回博客
语言: English 中文
Format 2026-05-30 9 分钟

YAML 何时会改变含义:类型、缩进与 Properties 转换

YAML 1.0 在 2001 年由 Clark Evans、Ingy döt Net 与 Oren Ben-Kiki 发布,目标是“对人友好”——XML 对机器友好、JSON 当时还不存在。愿景很美:一份“读起来像散文”的配置文件。现实是 YAML 1.1 规范里那张转换表造成的故障,比任何其他配置格式的任何单一特性都多——而 1.2 修了大部分,但你的工具大概率仍然在跑 1.1。

YAMLYAML 1.1YAML 1.2.propertiesNorway Problem配置

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。同样还有 NonoNnfalseFalseFALSEoffOffOFF。布尔转换表接受的输入范围荒谬地宽。挪威的两字母国家代码恰好在里面。Stack Overflow 问题、库 bug 报告、至少一起广为报道的部署事故,根因都同一个:YAML 1.1 以为 NO 是布尔

YAML 1.2 在 2009 年修了——布尔严格只 truefalse。但是你工具里很多解析器仍然默认 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 变成浮点丢掉零。
  • yesnoonoff 是布尔。
  • nullNullNULL~、(空) 都是 null。
  • 2010-04-01 是日期对象。版本号长得像日期就变日期。

模式:任何看起来像某种特殊类型的值,就被当成那个类型——除非加引号。安全的规则因此是:任何可能被误解的值,全部加引号version: "1.0"code: "NO"time: "12:34:56"port: "0123"。代价是轻微视觉噪声;收益是没有意外类型转换。

YAML 1.2 显著缩小了隐式类型集:只有 true/falsenull/~/空、整数、浮点会被转。NOyes12:34:562010-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,规模上还行,会以三种方式失败

  1. 和多文档流的组合奇怪。 anchor 是文档内的;跨文档传递需要扩展支持。
  2. dump 出去的内容不能安全地往返于任意工具。 多数工具读时解 alias,dump 出来失去结构。
  3. 被攻击者写的 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

互转

概念上:

  • .propertiesa.b.c=value → YAML {a: {b: {c: value}}}
  • a.b[0]=x(Spring 的列表扩展)→ {a: {b: [x]}}
  • YAML 的 map 展平成点分键;YAML 的列表展平成索引键(a[0]a[1])。

两个陷阱:

  1. 两边都丢类型。 YAML 的 enabled: true 是布尔 true。.properties 等价是 enabled=true,那是字符串"true"。消费者得知道何时去转。
  2. .properties 允许键名里含点而不是结构性。 web.url=http://... 既可能是"叫 web.url 的键",也可能是"web 的 url 子项"。YAML 转换得选一边,正确选择因应用而异

转换大体机械,本站工具按标准展平规则双向实现。它无法还原源里就没法表达的信息——.properties 没法编码带 anchor 引用的 YAML——但对"扁平配置 vs 嵌套配置"这种常见情形,往返干净。

常见坑

  • YAML 1.1 的布尔NOYesOff 当布尔。引起来或升级解析器。
  • 前导 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 工具

相关文章

继续阅读同一主题领域的实践指南。

查看全部文章

Cookie 同意

我们使用 Cookie 来增强您的体验并展示相关广告。您可以自定义您的偏好。