JsonMind
JSON 与数据13 分钟阅读更新于 2026年11月11日

YAML 缩进错误:修复 mapping values are not allowed here、Tab 与 Kubernetes 配置

修复 YAML 缩进错误:mapping values are not allowed here、Tab 与空格、块标量、列表缩进、锚点以及 Kubernetes 清单的常见写法。

JM作者:JsonMind Engineering最后复核: 2026年11月11日依据权威规范与引用来源复核。
快速答案

YAML 用空格和相对缩进表示层级,一个字符的偏移就可能改变文档树或让解析器停止。修复时先看报错行的上一行,检查 Tab、未加引号的冒号加空格、以及错位的短横线;然后校验全部文档、转成 JSON 查看真实结构,最后再做 Kubernetes schema 校验。

核心要点
  • 缩进就是结构:子块必须比父键更深,同级键必须从同一列开始。
  • YAML 缩进不允许 Tab,应把行首 Tab 换成空格,但不要改动块标量内容里的 Tab。
  • mapping values are not allowed here 通常指向未加引号的冒号加空格,或被缩进错误挂到标量下面的映射。
  • 块标量和多文档各有规则:内容缩进、chomping 指示符,以及必须位于第 0 列的 --- 分隔符。
  • 格式化器只能规范布局,不能修好语法合法但语义错误的树;应按语法、结构、schema 的顺序校验。

YAML 看起来只是一行行键值对,直到解析器拒绝一个“看起来没问题”的文件。报错行往往是解析器放弃的位置,而不是错误开始的位置。大多数 YAML 缩进错误是结构问题,而不是单纯的拼写问题:报错的那一行通常只是上方某一列变化的后果。少一个空格、从终端复制进来的 Tab、短横线偏移一列,或者值里有一个没加引号的冒号,都会改变解析器构建的树;同一个错误在 PyYAML、Go 和浏览器解析器里还会给出不同提示。

本文先说明缩进为什么等于结构,再逐一分析 “mapping values are not allowed here” 的原因、块标量、多文档、锚点和 Kubernetes 清单,最后给出一套把语法、结构和 schema 分开的排查顺序。

YAML 缩进如何定义结构

在块风格(block style)里,YAML 用缩进代替花括号。一个映射条目由键、冒号和值组成;值可以是行内标量,也可以是下一行的嵌套块。嵌套块必须比拥有它的键更深,同级键必须从同一列开始。解析器不关心两行看起来是否相关,只关心第二行是否比第一行更深。

规范没有规定必须用两个空格,但要求同一块内的层级保持一致。两个空格是最常见的约定;在同一个块里混用两格和四格,是让某一行“换父节点”的最快方式。

列表用短横线表示。短横线可以和父键同列,也可以比父键深一层,两种写法都合法,但同一个文件应统一:

  • ports: 后面跟同列的 - 80。
  • ports: 后面跟深两格的 - 80。

前一种写法让习惯“子级都要缩进”的读者感到意外,后一种更容易扫读。解析器并不在意短横线在哪一列,因为真正开始列表项的是短横线本身。

缩进是相对的,不是绝对的。给每一行都加一个空格不会改变结构;只给某一行加一个空格,就可能把它从映射变成标量,或从列表项变成父对象的字段。这就是为什么一个字符的修改会在很多行之后才报错。

像 YAML 格式化与校验工具 这样的纯语法工具,会报告块结构第一次说不通的行;浏览器里的解析器也会指出嵌套映射错误所在的具体行。

为什么 Tab 会破坏 YAML 缩进

YAML 里的 Tab 不能代替空格:规范禁止在缩进中使用 Tab。Tab 是一个控制字符,没有固定显示宽度;编辑器可以把它显示成两列、四列或八列,解析器无法把它当作稳定的结构列。Tab 可以出现在引号字符串里,也可以出现在块标量内容中,但行首用来表示层级的 Tab 永远不合法。

不同解析器的报错不一样。PyYAML 报 found character '\t' that cannot start any token;基于 libyaml 的工具措辞类似;Go 和 kubectl 常见的是 found character that cannot start any token 或 did not find expected key;浏览器解析器可能报映射条目缩进错误。这些提示通常不会直接写“Tab”,所以很容易被忽略。

改之前先让 Tab 可见。GNU grep 可以用 grep -nP '\t' file.yaml;macOS 和 Linux 的 sed -n '1,40l' file.yaml 会把 Tab 显示成 \t;VS Code 打开 Render Whitespace 后会画出箭头。项目里放一个 .editorconfig,设置 indent_style = space 和 indent_size = 2,可以从源头阻止大多数编辑器再次插入 Tab。

不要对整个文件做无脑的“Tab 转空格”。ConfigMap 里可能保存 Makefile,而 Makefile 的配方行必须使用真正的 Tab;全局替换会改变存储内容。只替换 YAML 结构行行首的 Tab,块标量内容要单独检查。

“mapping values are not allowed here”的具体原因

这句话来自 PyYAML 和其他基于 libyaml 的解析器。Go 工具常说 mapping values are not allowed in this context,yaml npm 包可能说 Nested mappings are not allowed in compact mappings。含义相同:扫描器在读取标量或其他不能接收映射值的节点时,遇到了一个“冒号加空白”。

报错里的冒号通常不是你以为的那个键。重点检查这些情况:

  • 普通标量里有未加引号的“冒号加空格”:tier: frontend: critical、url: https://example.com: 8443、message: Release: v2。第二个 : 会被当成标量内部的映射指示符。
  • 缩进把映射挂到了标量下面:name: web 的下一行写成 namespace: production,多了一个空格。标量 web 不能拥有映射。
  • 文档开头是普通标量,后面又出现映射:第一行 web,第二行第 0 列写 name: web。
  • 列表项本身是标量,后面的映射列不对:- web 后面跟 name: web。
  • 键的冒号后漏了空格:name:web 会被当成普通标量,不是键;下一行缩进的 labels: 就变成这个标量下面的映射,于是报错。
  • 键里出现第二个冒号:app: name: web 试图在同一行把 name: web 作为 app 的映射值。
  • Windows 路径里写了“冒号加空格”:path: C: \Users\me;C:\Users\me 没问题,因为冒号后面不是空格。

如果冒号属于文本,就给整个值加引号;如果这一行本来应该是同级键,就把缩进调回去。下面这份清单在第 tier 行失败:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
    tier: frontend: critical
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80
              protocol: TCP

PyYAML 会在第 7 行的第二个冒号处报错。文件其余部分结构是合法的,所以错误看起来很孤立:一个没加引号的冒号就够了。image: nginx:1.27 没问题,因为它的冒号后面是 1,不是空格。

列表缩进:避免短横线错位

列表最容易出现“只差一列”的错误,而在 Kubernetes 里,这种错误往往变成字段错误。短横线是结构指示符,不是装饰。对于“映射组成的列表”,第一个键从短横线加空格之后开始,同一个列表项里后面的键必须和第一个键对齐。如果 name 从第 10 列开始,image 也必须从第 10 列开始;第 9 列已经离开列表项,第 11 列则会变成更深一层的嵌套映射。

块列表可以和父键同列,也可以比父键深一层。ports: 后面紧跟同列的短横线合法,深两格也合法,解析器看到的是同一个列表。文件里选一种风格即可;yamllint 的 indentation 规则可以用 indent-sequences: true 或 consistent 来约束。

嵌套列表需要自己的短横线层级。参数列表是“标量列表”,容器列表是“映射列表”,每个容器里的端口列表又是另一个列表。每个短横线都要比拥有它的键更深,列表项内部的键则要互相对齐。

下面修正后的清单把冒号值加了引号,并保持每层两个空格。containers 列表缩进在 Pod 的 spec 下面,每个短横线再深两格,name、image、ports 从同一列开始:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
    tier: "frontend: critical"
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80
              protocol: TCP

如果 image 少了两个空格,YAML 仍然能解析:image 会变成 Pod spec 的字段,而不是容器的字段,kubectl 会报 unknown field "image"。解析错误和 schema 错误的区别,正是不能只看格式化结果、必须查看解析后树结构的原因。

块标量:|、>、chomping 与缩进指示符

块标量用来保存多行文本。| 是字面量(literal):内容里的换行和额外的行首空格都会保留。> 是折叠(folded):两行之间的单个换行变成空格,空行变成换行,缩进更深的行保留原有换行。脚本、配置文件和所有依赖换行的内容用 |;可以接受重新折行的说明文字才用 >。

块标量头部可以带 chomping 指示符。| 保留一个结尾换行,|- 去掉所有结尾换行,|+ 保留全部结尾换行;> 也可以用同样的后缀。指示符后面允许写注释,例如 | # keep line breaks。

内容必须比拥有它的键或父节点更深。第一个非空内容行的缩进决定基准,后面的非空行至少要达到这个深度。出现更浅的非空行时,块标量结束:如果这一行恰好是合法的同级键,块内容会被静默截断;如果它在该层级不是合法 YAML,解析器会报 could not find expected ':',或者把原本想写进内容的那一行当成映射而报错。

如果第一行内容本身以空格开头,或者内容以空行开头,就需要显式缩进指示符。|2 表示内容缩进是父缩进加两个空格,这样第一行额外的行首空格会被保留,而不会变成基准缩进。|2- 把缩进指示符和 strip chomping 组合起来;两个指示符的顺序可以互换,所以 |-2 也合法。

内容里的 Tab 在空格缩进完成之后是允许的,但它属于数据。ConfigMap 里保存的 Makefile 依赖这些 Tab,格式化器如果全局把 Tab 换成空格,YAML 仍然能解析,存储的文件却已经被改坏。下一节的多文档示例用 | 保存 nginx 配置。

普通标量与引号:冒号、井号和布尔值

普通标量(plain scalar)就是不加引号的文本。它不能包含“冒号加空格”,不能包含未加引号的“空格加 #”,也不能以 -、?、:、#、&、*、!、|、>、'、"、%、@、反引号、[、]、{、} 或 , 开头。它可以包含后面不跟空格的冒号,所以 nginx:1.27、12:30 和 https://example.com 都是合法的普通值。

# 只有在前面有空白或位于行首时才开始注释。image: nginx#1 里的井号属于值;image: nginx #1 才开始注释。含有 : 的值应该加引号,否则解析器会把冒号读成新的映射。单引号原样保留文本,用两个单引号表示一个单引号;双引号会解释 \n、\t 和 \uXXXX,需要转义时用双引号,Windows 路径和正则表达式更适合单引号。

类型推断是另一个坑。YAML 1.1 会把 yes、no、on、off 以及它们的大小写变体解析成布尔值;1.1 schema 还列出了单字母 y 和 n,但不同解析器的实现并不一致。它还会把 12:30 解析成六十进制整数 750,把 0777 解析成八进制数。YAML 1.2 core 只把 true 和 false 及其大小写变体当布尔值,把 yes、on 当字符串,取消六十进制,八进制改用 0o777。PyYAML 和许多旧的 Kubernetes 转换路径遵循 1.1;浏览器工具使用的 yaml npm 包遵循 1.2。同一份文件在不同工具里可能得到不同类型。

必须保持字符串的值一律加引号:country: "NO"、version: "1.20"、enabled: "yes"、time: "12:30"。在 Kubernetes 里,version: 1.20 会变成数字 1.2,字符串字段可能报 cannot unmarshal number into Go struct field;metadata.name: yes 会变成布尔值并报 cannot unmarshal bool。GitHub Actions 工作流里的 on 键,是同一个问题发生在键名上。

多文档文件与 --- 分隔符

一个 YAML 流可以包含多个文档。第 0 列的 --- 开始一个新文档,... 显式结束一个文档;... 之后的内容需要另一个 ---。文档标记不能缩进。在块标量里,缩进的 --- 是内容;第 0 列的 --- 则会结束块标量并开始新文档,即使你本来想把它当文本。

不同工具的接受方式不同。Python 的 yaml.safe_load 只读一个文档,发现第二个文档会抛 expected a single document in the stream;safe_load_all 会返回全部文档。浏览器的 YAML 格式化器会解析并格式化所有文档,再用 --- 重新拼接;JSON 与 YAML 转换器有意只处理一个文档,所以多文档文件要先拆分。

当解析器静默覆盖重复键时,漏写 --- 比普通语法错误更危险。两份都在第 0 列写 apiVersion、kind、metadata、spec 的清单会合并成一个映射,宽松的解析器会保留每个重复键的最后一个值,结果可能把第一份的 spec 和第二份的 metadata 拼在一起。Kubernetes 历史上使用过这种解析器,所以清单之间一定要显式分隔。

锚点和别名的作用域是单个文档。第一个 --- 之前定义的锚点,不能在后面的文档里引用;第二个文档需要自己的锚点定义。把共享的 defaults 块移到多文档文件顶部时,很容易踩到这个坑。

下面的流包含一个带字面量块标量的 ConfigMap,以及作为第二个文档的 Deployment。app.conf 的内容在键下面缩进四格,--- 位于第 0 列:

apiVersion: v1
kind: ConfigMap
metadata:
  name: web-config
data:
  app.conf: |
    server {
      listen 80;
      location / {
        proxy_pass http://web:8080;
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80

kubectl apply -f multi.yaml 会应用两个文档。如果删掉 ---,两个对象会合并,重复的 apiVersion 键由解析器决定保留哪个,而不是由 Kubernetes 决定。

锚点、别名与合并键的缩进陷阱

锚点以 &name 开头,附着在它后面的节点上;别名以 *name 开头,复制那个节点。锚点必须在同一文档里先于别名出现;未定义的别名会报 found undefined alias 或 unknown anchor,前向引用也不允许。

锚点不会改变缩进。写 defaults: &defaults 时,值是从下一行开始的、缩进更深的映射。如果子键和 defaults 写在同一列,它们就变成同级键,锚点附着在 null 上;<<: *defaults 要么因为合并需要映射或映射列表而报错,要么被当成值为 null 的普通 << 键,两种情况都不会发生合并。这个错误只差一列,锚点那一行本身看起来完全正确。

合并键写成 <<: *defaults,必须和目标映射里的其他键保持同一缩进。它是 YAML 1.1 的特性,不属于 YAML 1.2 core schema;PyYAML、js-yaml 和 Go 的 yaml 包支持它,一些严格解析器会把 << 当普通键或直接拒绝。Kubernetes 在校验前会把 YAML 转成 JSON,所以 API Server 通常看到的是合并后的字段,而不是合并键。

合并是浅合并。映射自身的键覆盖合并进来的键;别名写成列表时,前面的映射覆盖后面的映射。嵌套映射会整体替换,不会递归合并,所以从 defaults 合并 resources 时,可能把原来的 limits 和 requests 丢掉一半。

锚点适合在单个文档内复用,不适合跨文件或跨文档。转换成 JSON 时别名会被展开成普通值,输出变大,共享关系也消失;格式化器可能保留锚点语法,也可能展开,取决于工具。依赖锚点的 Kubernetes 清单在渲染输出里往往更难审查,因为合并结果已经看不出来了。

Kubernetes 缩进:apiVersion、kind、metadata 与 spec

Kubernetes YAML 缩进遵循同样的列规则,但清单多了一个固定的顶层形状:apiVersion、kind、metadata、spec 是第 0 列的同级键。metadata.name 和 metadata.labels 在 metadata 下面缩进两格,spec 回到第 0 列。如果把 metadata 缩进到 apiVersion 下面,解析器要么报错,要么把清单变成另一个对象;如果把 spec 缩进到 metadata 下面,kubectl 会报未知字段或缺少必填字段。

Deployment 多出两层容易混淆的结构。spec.template 是 Pod 模板,template.metadata 和 template.spec 是 template 下面的同级键。template.spec.containers 是对象列表,每一项以 - name 开始,image、ports、env、volumeMounts 都要和 name 对齐。列表项后续的键不能和短横线同列,少一列就会离开列表项,变成 Pod spec 的字段。

解析器和 schema 抓的是不同错误。image 缩进错了,YAML 可能仍然合法,但 kubectl 校验解析后的对象时会报 unknown field "image" in io.k8s.api.core.v1.PodSpec。ports 列表的第二项如果只写 protocol,YAML 合法,Kubernetes 会报这一项缺少 containerPort。Service 的 selector 是扁平映射,Deployment 的 selector 是带 matchLabels 的 LabelSelector;把一种结构套到另一种上,会得到未知字段错误,而不是缩进错误。

重复键是 Kubernetes 的静默风险。某些 YAML 转 JSON 路径会保留最后一个重复键而不报错,两行 image: 或两个 metadata.name 可能在校验前就被合并掉。带重复键规则的 linter 才是可靠检查;kubectl apply --dry-run=server 无法找回转换阶段已经丢弃的键。

YAML 1.1 的类型推断也会在这里出现。enabled: yes 会变成布尔值,字符串字段报 cannot unmarshal bool;version: 1.20 会变成数字 1.2,报 cannot unmarshal number。Kubernetes 期望字符串的字段,值要加引号。

Kubernetes YAML 检查工具 会解析每个文档,检查 apiVersion、kind、metadata.name 和 List 结构,能抓出缺失的顶层字段和多文档数量问题;它不做完整 schema 校验,所以还要配合 kubectl 或 kubeconform。

$ kubectl apply -f deployment-broken.yaml
error: error converting YAML to JSON: yaml: line 7: mapping values are not allowed in this context

$ kubectl apply -f deployment-misaligned.yaml
error: error validating "deployment-misaligned.yaml": error validating data: ValidationError(Deployment.spec.template.spec): unknown field "image" in io.k8s.api.core.v1.PodSpec; if you choose to ignore these errors, turn validation off with --validate=false

第一条错误是语法,第二条是结构。kubectl apply --dry-run=client -f file.yaml 不需要集群就能检查第一条,--dry-run=server 会按 API schema 检查第二条。字段落错对象时,可以用 kubectl explain deployment.spec.template.spec.containers 确认正确路径。

格式化器、排查流程与高频错误

格式化器不是修复器。它先解析文档,再用统一的缩进重新输出。输入无法解析时,它只能报错,无法猜出缺失的空格或引号应该放在哪里;输入能解析但某一行层级错了时,它会保留错误结构,只是让文件看起来更整齐。所以格式化过的清单仍然可能被 kubectl 拒绝。

格式化器还可能抹掉证据。有的会删除注释、展开锚点和别名、改变引号风格,或者只保留最后一个重复键。全局“Tab 转空格”会改写块标量里的 Tab,改变 Makefile 或配置内容。先用校验模式,再格式化,提交前把输出和源文件做 diff。

YAML 格式化与校验工具 会解析全部文档,指出语法错误的位置,并按两个空格重新格式化;解析器支持时会保留注释,遇到重复键会报错而不是静默选一个。语法干净之后,用 Kubernetes YAML 检查工具 检查清单形状,再用 JSON 与 YAML 转换器 把单个 YAML 文档转成 JSON,查看解析器实际构建的树。一次只转一个文档,多文档文件先拆分。

一套有效的排查顺序:

  1. 先读第一条错误和行号,检查上一个非空行,以及未加引号的值里有没有冒号。
  2. 用 grep -nP '\t'、sed -n '1,60l' 或编辑器的空白显示功能找出 Tab 和行尾空格漂移。
  3. 只用 YAML 格式化器或 python3 -c 'import yaml,sys; list(yaml.safe_load_all(open(sys.argv[1])))' file.yaml 校验语法;多文档要用 safe_load_all。
  4. 转成 JSON 检查嵌套。如果 image 在 spec 下面而不是 containers[0] 里面,说明语法通过但结构错了。
  5. Kubernetes 先跑 kubectl apply --dry-run=client,有集群再跑 --dry-run=server,并用 kubectl explain 或 kubeconform -strict -summary。
  6. 用 yamllint 检查 key-duplicates、truthy 和 indentation,把这些检查放进 CI,防止同类问题再次出现。
  7. 整块重新缩进,不要只改一行;只改一行常常留下同级键仍然错位。

高频错误包括:Tab、一空格漂移、key:value 少空格、未加引号的 value: with: colon、短横线和列表项第一个键错位、块标量内容缩进到键所在列、清单之间漏写 ---、跨文档使用锚点、重复键,以及未加引号的 yes、on 或版本号。它们大多在格式化结果里看不出来,却都能在解析树或 schema 校验的错误里暴露。

常见问题

为什么 YAML 缩进不能用 Tab?

YAML 规范禁止在缩进中使用 Tab,因为 Tab 没有固定宽度,不同编辑器会把它显示成不同列数,解析器无法把它当作稳定的结构列。Tab 可以出现在引号字符串和块标量内容里,但行首用于表示层级的 Tab 会让大多数解析器直接报错。

mapping values are not allowed here 应该怎么修?

找到报错指向的冒号,检查它所在的值以及上一行。如果未加引号的值里含有冒号加空格,就给整个值加引号;如果键的冒号后漏了空格,就补上;如果这一行本应是同级键,就把缩进调回去。报错行通常是解析器放弃的位置,真正的原因常常在上一行。

YAML 格式化器能自动修好缩进错误吗?

格式化器必须先解析成功才能重新输出。输入无法解析时,它只能报错,不能猜出缺失的空格或引号应该放在哪里。输入能解析但某一行的层级错了时,格式化器会保留这个错误结构,只是让它看起来更整齐,所以要先校验再格式化。

为什么 kubectl 没有提示清单里的重复键?

部分 Kubernetes 的 YAML 转 JSON 路径使用 YAML 1.1 解析器,遇到重复键时直接保留最后一个值,不会报错。重复的 image 或 metadata.name 可能在进入校验前就被覆盖。不同版本的 kubectl 行为不一致,应该在 CI 里用带重复键规则的 linter 检查。

Kubernetes 里对象列表应该怎么缩进?

短横线放在列表键下面,可以与键同列,也可以比键深一层;同一个列表项里的 name、image、ports 等键必须和短横线后的第一个键对齐。少一个空格,这个键就会离开列表项,变成父对象的字段,kubectl 会报 unknown field 或缺少必填字段。

参考资料

把指南用起来

浏览全部工具

打开浏览器工具。输入内容保留在浏览器中,不会上传到业务服务器。

相关指南

全部指南