DevKitLab Logo DevKitLab
YAML / 解析 / 调试 / 配置文件

YAML 为什么老出问题?响亮的报错和静默的坑

同一个早上来了两个工单,都说「我的 YAML 挂了」。一个是第 12 行的红色解析报错——一分钟就修好。另一个是服务正常启动、却跑出了错的行为,因为一个没加引号的值悄悄变成了别的东西。这是两个相反的问题,而危险的那个恰恰是安静的那个。

同一个早上来了两个工单,说的是同一句话:我的 YAML 挂了。

第一个是红色的解析报错——CI 日志指向第 12 行,「tab 不允许用作缩进」,一分钟不到就修好了。第二个更怪:服务干干净净地启动了,哪儿都没报错,可它服务的却是错的区域。盯了一个钟头才找到——有人给挪威写了 region: NO,而配置把它读成了布尔值 false。同一句抱怨,两个截然不同的问题。而第二种,恰恰是会溜到生产环境的那种——正因为没有任何东西「响亮」到能把它拦下来。

这就是关于 YAML 要弄明白的一点:「它挂了」是两个相反的失败,套着同一句话。 一种是响亮的——解析器拒绝这个文件,并且明确告诉你在哪儿。另一种是静默的——文件解析得好好的,却塞给你的程序一个你从没写过的值。响亮的那种浪费你十分钟;静默的那种会带上线。一旦你能分清眼前是哪一种,YAML 就不再像闹鬼了,因为这两种需要相反的排查动作。这是配置格式支柱页的姊妹篇——那篇讲了 YAML 爱猜是它便利的代价,这一篇把「猜」具体怎么咬人、又怎么和普通语法错误区分开,讲透。

响亮的失败:YAML 拒绝解析(而这是好消息)

当 YAML 拒绝一个文件时,你该庆幸。解析器已经替你抓到了问题,还递给你行号和列号。这些很烦,但不危险——你顺着指引,把语法改对就行。下面是你真正会碰到的几种。

用 tab 做缩进。 最常见的一种。YAML 禁止用 tab 字符缩进——结构只能用空格。

server:
	host: localhost

host 前面那个 tab,会一字不差地给你:

YAMLParseError: Tabs are not allowed as indentation at line 2, column 1

修法就是报错里说的那句:把 tab 换成空格。这一种最坑的时候,是编辑器悄悄插了 tab,或者复制粘贴把它带了进来——文件看上去缩进得好好的,只有解析器看得见那个 tab。

缩进不一致。 YAML 靠每行缩进多深来推断你的结构,所以多一个或少一个空格,就会改变谁嵌套在谁下面——而且往往变成一个解析器没法自洽的错误。

database:
  host: localhost
   port: 5432

porthost 多缩进了一个空格,解析器拒绝这种不可能的嵌套。修法是让同级的键严丝合缝地对齐到同一列。

未闭合的引号或括号。 流式集合([...]{...})和带引号的字符串必须闭合。

ports: [80, 443

会给你 Flow sequence … must be sufficiently indented(或者视后面内容而定,一个缺括号的错误)——而 name: "prod 会给 Missing closing "quote。两者都点名了行;两者都靠把你开的那个东西闭合来修。

重复的键。 同一个映射里两个同名的键是有歧义的,严格的解析器会直接拒绝:

region: us-east-1
region: eu-west-1

Map keys must be unique at line 2, column 1。(不是每个解析器都这么严——有的会悄悄留下最后一个值,这就把它悄悄滑进了另一类。当你的解析器真的拒绝它时,是在帮你的忙。)

这一节里的每一种失败,动作都一样:读报错,去它点名的行和列,在那儿把语法改对。 解析器站在你这边。接下来是它不站你这边的那一半。

静默的失败:YAML 解析得好好的,却给你错的值

这里没有红色报错,没有行号,没有可循的线索。文件「成功」解析了。问题在于,这个「成功」意味着别的东西,而不是你写的东西——一个字符串变成了布尔,一个空键变成了 null,一个版本号变成了数字。你的程序带着坏数据开跑,然后在离真正原因很远的某处崩掉。这是代价高昂的一种,它有好几副面孔。

冒号后面漏了空格。 一个键需要冒号后面跟一个空格(或者换行)。漏了这个空格,你不会得到报错——你会得到一个普通字符串:

timeout:30

解析成的是字符串 "timeout:30",而不是一个键 timeout 配上值 30。你的 config.timeoutundefined,而没有任何地方提过一句。

隐式 null。 一个后面什么都没有的键,不是空字符串——是 null~nullNullNULL 也一样:

retries:
fallback: ~
cache: null

这三个值全是 null。如果你以为 retries 会是空字符串、是 0、或者「不设就用默认」,你可能反而把一个货真价实的 null 递进了一段没做 null 检查的代码。这个值看着像缺失,其实它在场,而且是 null。

挪威问题,以及它的一众亲戚。 这就是支柱页里那个,也是静默失败的头号选手。在 YAML 1.1 下——像 PyYAML 这类解析器至今默认仍按它来——一堆裸词会变成布尔值:

region: NO      # 布尔值 false,不是国家代码 "NO"
enabled: yes    # 布尔值 true
debug: off      # 布尔值 false

yesnoonoffyn 以及它们各种大小写,全都会强转成布尔。YAML 1.2 的 core schema 去掉了这条——那里 NO 保持字符串——但很多运行时默认仍是 1.1,所以别想当然以为自己没事。(完整来龙去脉在支柱页里。

吃掉你字符串的数字。 任何长得像数字的东西都会变成数字,把让它作为文本才有意义的那部分丢掉:

version: 1.10   # 数字 1.1——末尾那个零没了
build:   1e5    # 数字 100000——被当成科学计数法
zip:     01234  # 1234(1.2 core)或 668(八进制,1.1)——前导零消失了
port:    0700   # 700(1.2 core)或 448(八进制,1.1)
time:    22:22  # 1.1 下变成 1342(六十进制:22×60 + 22);1.2 core 下是字符串

某个值到底会不会被强转、强转成什么,取决于你用的解析器和它套用的 schema——在采用这些隐式类型规则的解析器下,它们会在不报错的情况下改变类型(比如 22:22 在 YAML 1.2 的 core schema 下仍是字符串,在 1.1 下却变成 1342)。一处本来等着 "1.10" 的版本校验,拿去和 1.1 比对、然后判定两者不等;一个零填充的 ID,悄悄丢了它的零。

这些写法通常都不会报错,也就没有可追踪的行号——在解析器看来什么都没出错。所以排查动作不能是「顺着报错走」——根本没有报错。它只能换成一个习惯。

两种各自怎么排查

整件事的关键,就是把这两种分开,而判据只有一问:你到底有没有拿到解析报错?

  • 你拿到了报错。 那是响亮的失败。报错里带着行和列——到它指出的位置,把语法改对。tab 换空格、同级对齐、闭合引号和括号、给重复键改名。顶多十分钟。
  • 它解析过了,可下游有什么不对劲。 那是静默的失败,没有行可循。别去把整个文件重读一遍找错字——而要看你程序实际收到的那个值,倒着往回推。一个你本以为是文本的地方冒出个 false、一个你本以为是键的地方冒出个 null、一个你本以为是版本串的地方冒出个数字:每一个都直指一个被强转的、没加引号的标量。修法几乎永远是同一个习惯——凡是人会读成文本的,都加引号:国家代码、版本号、带前导零的 ID、时间,以及 yes/no/on/off 那一家子。
region: "NO"
version: "1.10"
zip: "01234"

加引号不花什么成本,却能把那个值上的「猜」关掉。

还有个更快的办法,能在它们上线之前就看见那些静默的坑。把文件贴进 YAML 转换器,读它的检查面板:它默认按 YAML 1.2 解析,但会专门标出上面那些坑里的几种——YAML 1.1 布尔陷阱(那个没加引号的 no)、即将塌成浮点的版本号、超过 2^53 的大整数、用作缩进的 tab——连同它们会改成什么值一起点出来。把同一个文件转成 JSON 能兜住其余的:你在 JSON 输出里看到的类型,就是你程序真正会拿到的类型,一个隐式的 null、一个被当成字符串的 timeout:30,都会现出原形。

收尾清单

下次 YAML「挂了」,先问它是哪一种:

  1. 有没有解析报错? 有,那就是响亮又安全的——去它点名的行和列。tab 换空格、修好缩进、闭合引号或括号、给重复键去重。
  2. 它解析过了、行为却不对? 那是静默的。没有行可循——去查你程序收到的那个值,把它追回到那个被强转的标量。
  3. 给有歧义的标量加引号。 国家代码、版本号、带前导零的 ID、时间,以及 yes/no/on/off 那一家子。加引号会关掉 YAML 在那个值上的「猜」——这是对付静默失败最有效的一个习惯。
  4. 上线前先过一遍检查面板。 YAML 转换器会把静默的强制转换提前摆出来,瞄一眼 JSON 转换结果,你就看到了真实类型。

一句话记住:那些能拦住你构建的 YAML 报错,是你该庆幸能拿到的。 它们只花你几分钟,而且会自己指出自己。真正让你搭进一个下午的,是那种一声不吭就解析过去、悄悄递给你程序一个 false、一个 null、或一个被抹掉尾数的数字的 YAML。学会去怀疑那个安静的「成功」,而不只是那个响亮的失败,YAML 的大半闹鬼也就散了。