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

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

**快速答案:** 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 格式化与校验工具](/tools/yaml-formatter) 这样的纯语法工具，会报告块结构第一次说不通的行；浏览器里的解析器也会指出嵌套映射错误所在的具体行。

## 为什么 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` 行失败：

```yaml
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` 从同一列开始：

```yaml
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 列：

```yaml
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 检查工具](/tools/kubernetes-yaml) 会解析每个文档，检查 `apiVersion`、`kind`、`metadata.name` 和 `List` 结构，能抓出缺失的顶层字段和多文档数量问题；它不做完整 schema 校验，所以还要配合 kubectl 或 kubeconform。

```text
$ 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 格式化与校验工具](/tools/yaml-formatter) 会解析全部文档，指出语法错误的位置，并按两个空格重新格式化；解析器支持时会保留注释，遇到重复键会报错而不是静默选一个。语法干净之后，用 [Kubernetes YAML 检查工具](/tools/kubernetes-yaml) 检查清单形状，再用 [JSON 与 YAML 转换器](/tools/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 或缺少必填字段。

## 参考资料

- [YAML 1.2.2 规范](https://yaml.org/spec/1.2.2/)
- [YAML 1.1 布尔类型](https://yaml.org/type/bool.html)
- [Kubernetes 配置最佳实践](https://kubernetes.io/docs/concepts/configuration/overview/)
- [yamllint 文档](https://yamllint.readthedocs.io/en/stable/)

---

- Canonical: https://www.jsonmind.com/zh-CN/guides/yaml-indentation-error
- Locale: zh-CN
- Published: 2026-11-11
- Updated: 2026-11-11
- Tools: yaml-formatter, kubernetes-yaml, json-yaml