YAML 常见语法错误排查:缩进、冒号、特殊字符、Tab
YAML 报错绝大多数出在五个地方:用了 Tab 缩进、冒号后面没有空格、值里带了冒号或井号没加引号、缩进层级对不齐、以及值以 @、*、% 这类特殊字符开头。更麻烦的是另一类"不报错但值不对"的情况:yes 变成 true、001234 变成 668。本文把这些问题逐一复现,贴出真实解析器的报错原文和修法。所有报错都是我在 Spring Boot 3.5.7(内置 SnakeYAML 2.4)里实际跑出来的。
一、先学会读报错信息
SnakeYAML 的报错固定带一句 in 'reader', line N, column M,行号和列号都从 1 开始数,下面还会把出错那一行连同一个 ^ 指针打印出来。读报错时记住两点:
- 真正的错误常常在报错行的上一行。解析器是在"读不下去"的地方报错的,而不是在"写错了"的地方。比如上一行少了一个空格,下一行才会触发报错。
- 报错措辞因解析器而异。Spring Boot 和本站首页的 Properties ⇄ YAML 转换器用的是 SnakeYAML;本站的 YAML ⇄ JSON 工具 用的是 js-yaml 4.1.0;Python 常用 PyYAML。同一个错误,三个解析器的说法不一样(下面会对照),但指向的位置基本一致。
先给一张速查表,后面逐条展开:
| 现象 | SnakeYAML 报错关键词 | 常见原因 |
|---|---|---|
| 第 N 行 Tab | found character '\t(TAB)' that cannot start any token | 用 Tab 缩进 |
| 某行下一个键报错 | mapping values are not allowed here | 值里有 : ,或这一行缩进多了 |
| 键值全连在一起 | could not find expected ':' | 冒号后少了空格 |
| 某个值以特殊符号开头 | cannot start any token / while scanning an alias | @ % 反引号 * 开头的值没加引号 |
| 引号相关 | while scanning a quoted scalar | 引号没闭合,或双引号里有非法转义 |
| 列表括号相关 | while parsing a flow sequence | [ 没写 ] |
| 同名键 | found duplicate key | 同一层级重复写了同一个键 |
二、Tab 缩进:YAML 不允许
YAML 规范明确规定缩进只能用空格。编辑器里 Tab 和四个空格看起来一样,是最隐蔽的错误来源。下面第二行是以 Tab 开头的:
server:
port: 8080
while scanning for the next token
found character '\t(TAB)' that cannot start any token. (Do not use \t(TAB) for indentation)
in 'reader', line 2, column 1:
port: 8080
^
其他解析器的说法:js-yaml 是 tab characters must not be used in indentation (2:1),PyYAML 是 found character '\t' that cannot start any token。三者都指向第 2 行第 1 列。
修法:把 Tab 换成空格。在编辑器里打开"显示空白字符",并在 VS Code 里确认右下角是 Spaces 而不是 Tab Size;也可以在项目根目录放一份 .editorconfig,对 yml 文件指定 indent_style = space。
三、冒号后缺空格:有时报错,有时静默
YAML 里键和值之间必须是 冒号 + 空格。如果写成 port:8080,解析器会把整个 port:8080 当作一个普通字符串。这个行为有两种后果。
如果这是该层级里唯一的一行,不会报错,只是值变了:
server:
port:8080
Spring Boot、PyYAML、js-yaml 都把它解析成 server = "port:8080",而你想要的是 server.port = 8080。启动后的现象往往是"端口配置没生效",找起来很费劲。
如果同一层级还有别的键,就会报错:
server:
port: 8080
name:demo
host: localhost
while scanning a simple key
in 'reader', line 3, column 3:
name:demo
^
could not find expected ':'
in 'reader', line 4, column 3:
host: localhost
^
注意它指向的是第 3 行的 name:demo,那里才是错误所在,但报错同时给出了第 4 行的位置。js-yaml 对这一段的说法是 can not read a block mapping entry; a multiline key may not be an implicit key (4:7),更难直接看懂。修法:补上空格,写成 name: demo。
四、值里含冒号或井号:要加引号
普通(不带引号)的值里,出现"冒号后跟空格"就会被当作新的键值对:
app:
title: Hello: World
mapping values are not allowed here
in 'reader', line 2, column 15:
title: Hello: World
^
js-yaml 的说法是 bad indentation of a mapping entry (2:15),PyYAML 和 SnakeYAML 一致,都是 mapping values are not allowed here。很多人看到 bad indentation 去检查缩进,其实是值里有冒号。修法:整个值用引号括起来:
app:
title: "Hello: World"
反过来,: 后面不跟空格是安全的:JDBC 地址 jdbc:mysql://localhost:3306/db?useSSL=false、时间戳字符串里的冒号都不用加引号(后一种要小心类型,见第九节)。
# 的规则类似:只有"空格 + #"才是注释的开始。下面这段不会报错,但密码被截断了:
app:
password: abc #def
token: a#b
读出来 app.password 是 abc(#def 被当成注释丢掉了),而 app.token 是完整的 a#b。含 # 的密码、密钥务必加引号。
五、以特殊字符开头的值
YAML 把一部分符号保留作语法用途,值以它们开头时必须加引号。我实测了五个最常遇到的:
| 写法 | SnakeYAML 报错 | 典型场景 |
|---|---|---|
| mail: @company.com | found character '@' that cannot start any token | Maven 资源过滤 @project.version@ |
| pattern: *.log | while scanning an alias ... unexpected character found .(46) | 通配符、日志路径 |
| format: %d items | found character '%' that cannot start any token | 格式化模板 |
| cmd: `ls` | found character '`' that cannot start any token | 粘贴过来的 shell 命令 |
| v: !important | could not determine a constructor for the tag !important | 以 ! 开头被当成类型标签 |
以 @ 开头的完整报错如下:
app:
mail: @company.com
while scanning for the next token
found character '@' that cannot start any token. (Do not use @ for indentation)
in 'reader', line 2, column 9:
mail: @company.com
^
以 * 开头会被当成"引用别名",因为 YAML 用 &名字 定义锚点、*名字 引用它。修法:统一加单引号:mail: '@company.com'、pattern: '*.log'。另外,{、[、|、>、&、?、以及"短横线加空格"开头的值也有特殊含义,同样建议加引号。
六、缩进对不齐
同一层级的键必须缩进相同。下面第 3 行多缩进了一个空格:
app:
name: x
port: 1
mapping values are not allowed here
in 'reader', line 3, column 8:
port: 1
^
这条报错和第四节的"值里有冒号"是同一句 mapping values are not allowed here,因为在解析器看来,name: x 后面的缩进更深的内容是 x 这个值的延续,延续里出现了冒号。看到这句报错,先检查报错行和它上一行的缩进是否一致,再检查值里有没有冒号。
列表短横线对不齐:静默出错
列表更危险,因为有些错位不会报错:
app:
hosts:
- a
- b
Spring Boot、PyYAML、js-yaml 都不报错,三个解析器都把它读成只有一个元素的列表,元素内容是字符串 "a - b"。你本来想要的是两个元素 a 和 b。列表写完以后数一数元素个数,或者用 YAML ⇄ JSON 工具 转成 JSON 看一眼数组长度,能避开这类错误。
另外一个常见错误是在键值对之间混入列表项:
app:
a: 1
- b
while parsing a block mapping
in 'reader', line 2, column 3:
a: 1
^
expected <block end>, but found '-'
in 'reader', line 3, column 3:
- b
^
七、引号:没闭合、转义和单双引号的区别
双引号没有闭合时,解析器会一直读到文件末尾才报错,所以报错行是文件的最后一行,而不是引号所在的行:
app:
msg: "hello
port: 1
while scanning a quoted scalar
in 'reader', line 2, column 8:
msg: "hello
^
found unexpected end of stream
in 'reader', line 4, column 1:
^
可以看到第一段指向第 2 行的引号起点,第二段指向文件末尾。看到 found unexpected end of stream 就去找没闭合的引号。
两种引号的区别需要记住:双引号里反斜杠是转义符,单引号里不是。Windows 路径放在双引号里会触发错误:
app:
dir: "C:\temp\x"
while scanning a double-quoted scalar
in 'reader', line 2, column 8:
dir: "C:\temp\x"
^
expected escape sequence of 2 hexadecimal numbers, but found: "
in 'reader', line 2, column 18:
dir: "C:\temp\x"
^
用单引号或者不加引号即可,下面这样写,值就是原样的 C:\temp\x:
app:
dir: 'C:\temp\x'
单引号字符串里要表示一个单引号,写两个:'it''s' 得到 it's;双引号字符串里表示双引号用 \":"say \"hi\"" 得到 say "hi"。我用 PyYAML 验证过这两个例子的结果。
八、方括号、花括号与重复键
行内写法 [a, b] 少了右括号时,报错会落在下一行:
app:
hosts: [a, b
port: 1
while parsing a flow sequence
in 'reader', line 2, column 10:
hosts: [a, b
^
expected ',' or ']', but got :
in 'reader', line 3, column 7:
port: 1
^
重复键是个值得单独说的问题,因为不同解析器的反应完全不同:
app:
name: a
app:
port: 1
while constructing a mapping
in 'reader', line 1, column 1:
app:
^
found duplicate key app
in 'reader', line 3, column 1:
app:
^
js-yaml 同样报错:duplicated mapping key (3:1)。但 PyYAML 不报错,读出来是 {'app': {'port': 1}}——第一段的 name 悄悄丢失。如果你用 Python 脚本校验配置,要么换成严格模式的库,要么配合 yamllint(它会报 duplication of key "app" in mapping)。
九、不报错,但值被改了:类型陷阱
这是最需要警惕的一类。Spring Boot 的 SnakeYAML 实现 YAML 1.1,规则比较"热心";js-yaml 4 按 YAML 1.2 的核心规则,要保守得多。下面是同一份输入在三个解析器里的实测结果(Spring Boot 3.5.7 / PyYAML 6.0.3 / js-yaml 4.1.0):
| 写法 | Spring Boot | PyYAML | js-yaml(本站工具) |
|---|---|---|---|
| enabled: yes | true | True | "yes" |
| mode: off | false | False | "off" |
| country: NO | false | False | "NO" |
| flag: y | "y" | "y" | "y" |
| zip: 001234 | 668 | 668 | 1234 |
| zip: 08 | "08" | "08" | 8 |
| code: 010 | 8 | 8 | 10 |
| time: 12:30:45 | 45045 | 45045 | "12:30:45" |
| ver: 3.10 | 3.1 | 3.1 | 3.1 |
| n: 1e3 | 1000.0 | "1e3" | 1000 |
| d: 2026-10-20 | "2026-10-20" | date 对象 | Date 对象 |
| v: ~(或 v: 留空) | ""(空字符串) | None | null |
几点解读:
- 布尔字样。YAML 1.1 把
yes/no/on/off(及大小写变体)当布尔值,这就是著名的"挪威问题":国家代码NO被读成false。y、n倒不会被转换。 - 以 0 开头的数字。
001234按八进制读成 668;08因为含 8,不是合法八进制,保持字符串。工号、邮编、手机区号这类必须保留前导零的值,一定要加引号。 - 冒号分隔的时间。
12:30:45在 YAML 1.1 里是"六十进制整数",读成 45045。 - 版本号。
3.10在三个解析器里都是浮点数 3.1,末尾的 0 丢了。1.10.0这种多段版本号倒是字符串,不受影响。
解决办法只有一个:想要字符串就加引号。country: "NO"、zip: "001234"、ver: "3.10"。引号只影响类型,不会变成值的一部分。
同一份 YAML,在线工具和 Spring Boot 的结果可能不同。用 YAML ⇄ JSON 看到 "yes",不代表 Spring Boot 里读到的也是字符串。要确保稳妥,写配置时就给歧义值加引号。
十、多行字符串:| 和 >
长文本(SQL、证书、说明文字)不要硬塞在一行里。用 | 保留换行,用 > 把换行折叠成空格:
a: |
x
y
b: |-
x
y
c: >-
p
q
用 PyYAML 解析的结果:a 是 "x\ny\n",b 是 "x\ny",c 是 "p q"。要点:| 和 > 默认会在末尾保留一个换行,后面加 -(即 |-、>-)可以去掉它。块里的缩进必须比键更深,且块内每行缩进要一致。
十一、一套排查流程
- 先看行号,再看上一行。报错行附近两三行都要看。
- 搜 Tab。在编辑器里搜索正则
\t,有命中就是 Tab 问题。 - 找出值里的
:和#。它们是"mapping values are not allowed here"最常见的原因。 - 二分法缩小范围。把文件前半部分保留、后半部分注释掉,看报错是否消失,几次就能定位。
- 用工具对照。把 YAML 粘贴到 YAML ⇄ JSON 工具,右边的 JSON 能直接看出层级和数组长度对不对;Properties 配置则用首页的 Properties ⇄ YAML 转换器,转换结果与预期不符时,往往就是缩进或引号的问题。
- 上 lint。命令行可以用
yamllint(pip install yamllint)。我对第二节的 Tab 例子运行它,输出2:1 error syntax error: found character '\t' that cannot start any token (syntax);对重复键则输出duplication of key "port" in mapping。它还会对yes这类歧义值给出truthy value should be one of [false, true]的警告,正好补上"不报错但值不对"的盲区。
配置里嵌套了 JSON 时,也可以用 JSON 格式化 / 校验 先把 JSON 部分单独验证一遍,再贴回 YAML。
读完就动手试试:粘贴你的配置,一键转换并检查结果。