YAML Norway 问题与工程师必知的 JSON ↔ YAML 差异
一次常规的 Helm 部署。团队花了两天时间调整一个多区域发布用的 values.yaml 文件。这个 chart 将一个包含 locale 元数据的 Kubernetes ConfigMap 模板化——其中包括挪威数据中心的国家代码。有人在文件里写了 country: NO 并提交。CI 流水线显示绿色。部署顺利推出。
然后,告警来了。
ConfigMap 里的内容变成了 country: false,而非 country: "NO"。所有读取 country 字段的下游服务拿到的是布尔值而不是字符串。字符串比较失败,路由逻辑回落到默认值,本该留在挪威处理的流量却被路由到了错误的区域端点。
根本原因:YAML 文件里一个未加引号的字符串。YAML 1.1——几乎所有 Kubernetes 工具链使用的版本——将 NO 解析为布尔值 false。YES、ON、OFF、Y、N、no、yes、on、off、y、n 以及其他十几个变体也是如此。没有警告,没有报错,静默出错。
JSON 不存在这个问题。{"country": "NO"} 永远是字符串。YAML 的隐式类型推断,既是最大的便利,也是最危险的隐患。
本文涵盖完整图景:Norway 问题为何存在,YAML 1.2 做了哪些改变(以及为何大多数工具仍在忽视它),如何选择正确的引号策略,新手常踩的缩进陷阱,数字精度问题,以及四个真实转换场景——从 Kubernetes 清单到 Terraform 计划。需要安全地将 JSON 值转换为 YAML 而不踩 Norway 陷阱时,我们的 JSON 转 YAML 转换器会自动为 Norway 风险字符串加引号。
JSON vs YAML — 何时选哪个
在深入 Norway 问题之前,先了解两种格式各自的设计重心。它们并不可以互换——每种格式都有使其成为特定场景最佳选择的设计核心。
| 维度 | JSON | YAML |
|---|---|---|
| 语法 | 严格——花括号、引号、逗号缺一不可 | 灵活——缩进驱动,标点极少 |
| 类型系统 | 显式:string、number、boolean、null、array、object | 隐式——YAML 1.1 根据值的形态推断类型 |
| 人类可读性 | 开发者友好,机器可验证 | 人类友好,便于手动编辑 |
| 引号要求 | 字符串必须加引号 | 大多数标量可以不加引号(Norway 问题的根源) |
| 注释 | 不支持 | 支持 # 注释 |
| 主要用途 | API、数据交换、现代配置系统 | Kubernetes、Docker Compose、Ansible、CI 流水线 |
| 意外解析 | 无——严格解析 | 有——Norway、八进制、时间戳 |
| Schema 验证 | JSON Schema 生态完善 | YAML Schema(工具较少) |
JSON 胜出的场景:数据跨系统边界流转——REST API、消息队列、数据库序列化。机器产生、机器消费,严格的语法让验证变得直接。发送前用 JSON 格式化工具验证结构。
YAML 胜出的场景:人类是主要作者。Kubernetes 清单、GitHub Actions 工作流、Helm chart、Ansible playbook——这些都是开发者每天读写数十次的文件。减少标点和支持注释,让它们比 JSON 等价物更易维护。
问题出现在边界处:工具产生 JSON(如 kubectl get deploy -o json 或 terraform show -json),而人类需要将结果以 YAML 形式纳入版本控制或编辑。Norway 问题就藏在这个转换环节。反向转换时,我们的 YAML 转 JSON 转换器可以处理回程方向。
Norway 问题——深度解析
Norway 问题不是 bug。它是 YAML 1.1 规范完全按设计运行的结果。理解它为何如此设计——以及为何如此多系统仍在实现 1.1——是避开它的关键。
为什么 “no”、“yes”、“on”、“off”、“y”、“n” 会被误解析
YAML 1.1 规范定义了一种宽泛的布尔类型,初衷是让配置对人类友好。它将以下所有值识别为 true 或 false:
True: y、Y、yes、Yes、YES、true、True、TRUE、on、On、ON
False: n、N、no、No、NO、false、False、FALSE、off、Off、OFF
设计初衷是好的:英语配置文件中常用 yes/no 代替 true/false,YAML 希望支持这种自然的写法。问题在于,yes、no、on、off、y、n 在大多数应用中也是完全合法的字符串,含义截然不同。
具体 YAML 示例中的这种错配:
# YAML 1.1(大多数解析器的实现)
country: NO # 解析为:country: false ← 危险
enabled: yes # 解析为:enabled: true
restart: off # 解析为:restart: false
language: y # 解析为:language: true
shell: n # 解析为:shell: false
# 正确——显式字符串引号覆盖类型推断
country: "NO" # 解析为:country: "NO" ← 安全
enabled: "yes" # 解析为:enabled: "yes"
restart: "off" # 解析为:restart: "off"
language: "y" # 解析为:language: "y"
shell: "n" # 解析为:shell: "n"
对应的 JSON 写法:
{"country": "NO"}
在 JSON 中,引号内的 NO 永远是字符串,毫无例外,不存在隐式类型推断。JSON 看似繁琐的严格性,恰恰是其安全的来源。
除布尔值强制转换外,YAML 1.1 还会隐式转换:
123e4→ 数字1230000(科学记数法)0x1A→ 数字26(十六进制)0755→ 数字493(八进制——这会破坏 Unix 文件权限字符串)2024-05-04→ 许多解析器中的日期对象(而非字符串)1_000_000→ 数字1000000(下划线分隔符)
YAML Norway 问题,不过是 YAML 隐式类型强制转换这整个家族中最著名的一员。
YAML 1.1 vs 1.2 — 变了什么
YAML 1.2 于 2009 年发布——距 YAML 1.1 发布四年之后。其主要目标是将 YAML 与 JSON 严格对齐(因为 JSON 实际上是 YAML 1.2 的有效子集),并减少令人意外的隐式类型转换。
在 YAML 1.2 中:
- 布尔值严格收窄为
true和false(大小写敏感),仅此两个。yes、no、on、off都是普通字符串。 - 八进制字面量需要
0o前缀(0o755)——旧的0755形式被解析为字符串。 - 时间戳不再隐式解析——
2024-05-04保持字符串形式,除非显式标注类型标签。 - 规范本身是 JSON 的超集,即所有有效的 JSON 文档都是有效的 YAML 1.2。
从规范层面看,YAML 1.2 完全解决了 Norway 问题。实践中,生态系统几乎原地踏步。
| 库 | 默认规范 | Norway 风险 |
|---|---|---|
| PyYAML(Python) | YAML 1.1 | 有——yaml.safe_load 仍将 NO 解析为 False |
| ruamel.yaml(Python) | YAML 1.2(可选) | 可配置——默认更安全 |
| js-yaml(Node.js) | YAML 1.1 | 旧版本有;较新版本有 FAILSAFE_SCHEMA 选项 |
| eemeli/yaml(Node.js) | YAML 1.2 | 无——默认 1.2,或可显式选择版本 |
| gopkg.in/yaml.v2(Go) | YAML 1.1 | 有 |
| gopkg.in/yaml.v3(Go) | YAML 1.2 | 显著更安全 |
| Kubernetes / Helm | YAML 1.1(通过 Go yaml.v2) | 有——历史遗留,迁移极其困难 |
| Ansible | YAML 1.1(通过 PyYAML) | 有 |
迁移缓慢的原因是向后兼容性。依赖 yes/no 解析为布尔值已有十年之久的系统,无法在不破坏现有配置的情况下静默更改这一行为。Kubernetes 的装机量极其庞大,更改 YAML 解析语义将是集群范围内的破坏性变更。
实践结论: 除非你明确配置过,否则假定任何工具都使用 YAML 1.1 语义。对所有可能被误读为布尔值、时间戳或数字的字符串,一律加引号。
生产系统如何中招
挪威国家代码是被引用最多的例子,因为它反直觉——NO 看起来明显是缩写,而非布尔值。但这种模式在许多真实场景中反复出现:
IATA 机场代码。 挪威 Harstad/Narvik 机场的代码是 EVE,安全。奥斯陆加德莫恩机场是 OSL,也安全。但任何使用 YAML 存储区域机场代码的应用,都与把 no 路线代码变成布尔值 false 只差一步。
环境变量值。 在某些老旧系统中,ON 是完全合法的环境变量值,表示”启用”;OFF 是对应值。将配置从 shell 脚本迁移到 YAML 时,若不对这些值加引号,就会引入静默类型强制转换。
邮件用户字段。 名字或用户名恰好是 n、y 或任何触发词的用户,在应用程序不加引号地序列化 YAML 时会出现错误。这种情况特别隐蔽,因为只有部分用户会受影响。
Docker Compose 重启策略。 restart_policy 字段的值 "no" 意味着”不重启”。如果在 YAML 往返过程中丢失了引号,值就变成 false,Docker Compose 可能将其解释为”未指定重启策略”或抛出验证错误——无论哪种,容器重启行为都是错的。
GitHub Actions 的 shell: 字段。 有效的 shell 值是 bash、pwsh、python、sh、cmd、powershell,这些都不是 Norway 触发词。但如果有人在草稿编辑期间写了 shell: yes 或 shell: on 作为占位符,可能会惊讶地发现 YAML 在验证器看到它之前就已经把它变成了布尔值。
所有情况的解决方法相同:对语义上是字符串的值加引号,无论人类是否会把它认出来是关键字。我们的 JSON 转 YAML 转换器会自动处理——Norway 词汇表中的任何值在输出中都会被加引号。
字符串引号策略
理解了 Norway 词汇为何错配之后,解决方案就是为你的用途选择正确的引号策略。YAML 支持三种模式,各有不同的权衡。
自动 vs 双引号 vs 单引号
自动引号(推荐用于大多数转换场景)让库来决定何时需要引号。不加引号会被误读的值——Norway 词、数字、时间戳、看起来像 YAML 语法的字符串——会自动加引号。其他值保持普通标量形式。这在保证安全的同时,产生最具可读性的输出。
# 自动模式输出
name: Alice # 普通——无歧义
country: "NO" # 加引号——Norway 词
age: 30 # 普通——无歧义的数字
created: "2024-05-04" # 加引号——否则会被解析为日期
port: "8080" # 取决于库——部分库会对数字型字符串加引号
双引号字符串对所有字符串值都使用双引号。这种方式明确、可审计——任何读者都能一眼看出所有值都是字符串,无需理解规范。代价是冗长,对深层嵌套的配置来说可读性下降。
# 双引号模式
name: "Alice"
country: "NO"
replicas: "3" # 连数字也变成字符串——可能导致 schema 错误
注意:如果目标 schema 期望数字类型,而你将其序列化为加引号的字符串,YAML 解析器会正确地将其类型识别为字符串,但 Kubernetes 或其他严格的消费方可能会因字段类型不正确而拒绝它。
单引号字符串是 YAML 特有的特性——JSON 没有单引号语法。单引号是字面量:内部不支持转义序列。唯一的特殊情况是,单引号内的单引号必须用双写 ('') 表示。单引号非常适合包含反斜杠或在双引号中需要转义的特殊字符的字符串。
# 单引号模式
pattern: 'C:\Users\alice\Documents' # 无需转义
regex: '\d+\.\d+' # 反斜杠是字面量
对于需要往返回 JSON 的 JSON 转 YAML 转换,优先选择自动或双引号模式。单引号字符串引入了 YAML 特有的语法,返回时需要能感知 YAML 的解析器。
块标量(| 和 >)
YAML 的块标量语法对多行字符串确实非常有用——JSON 处理多行字符串时必须使用 \n 转义序列,非常别扭。
字面块标量 | 精确保留换行符:
# 字面块——保留换行符
script: |
#!/bin/bash
set -euo pipefail
echo "Starting deployment"
kubectl apply -f manifest.yaml
# 等价的 JSON 表示(不可读)
# {"script": "#!/bin/bash\nset -euo pipefail\necho \"Starting deployment\"\nkubectl apply -f manifest.yaml\n"}
折叠块标量 > 将行与行之间用空格连接,每个换行变成空格(空行除外,空行变成换行):
# 折叠块——换行变空格
description: >
This service handles authentication
for the entire platform. It supports
OAuth2, SAML, and API key authentication.
# 结果:"This service handles authentication for the entire platform. It supports OAuth2, SAML, and API key authentication.\n"
块标量非常适合在 YAML 配置中嵌入 TLS 证书、多行 shell 脚本或 SQL 查询——这些场景下,JSON 等价物会是一行长长的转义字符串,无人能读懂。
从 JSON 转换为 YAML 时,大多数转换器(包括我们的工具)使用自动模式,只有检测到嵌入换行符时才使用块标量表示多行字符串。单行字符串使用流式标量(加引号或普通形式)。使用我们的 JSON 转 YAML 转换器在提交清单之前预览输出。
缩进——2 vs 4 空格,禁止制表符
YAML 的缩进规则比看上去更严格。规范有一条绝对规则和一条因生态系统而异的惯例。
绝对规则:禁止制表符。 每一层缩进必须使用空格。YAML 文件中的制表符在大多数解析器中是解析错误:
# 错误——制表符导致解析错误
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app # ← 此处有制表符 → ParseError
# 正确——只用空格
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app # ← 两个空格
不同库报告的错误信息不同。Python 的 PyYAML:
yaml.scanner.ScannerError: while scanning for the next token
found character '\t' that cannot start any token
Go 的 yaml.v3:
yaml: line 4: found character that cannot start any token
请配置编辑器将 YAML 文件中的制表符展开为空格。VS Code 中,在工作区设置里添加:"[yaml]": { "editor.insertSpaces": true, "editor.tabSize": 2 }。
惯例:2 vs 4 空格。 两者都合法。不同生态系统的惯例不同:
| 生态系统 | 惯例 | 原因 |
|---|---|---|
| Kubernetes 清单 | 2 空格 | 官方文档和示例均使用 2 |
| Helm chart | 2 空格 | 遵循 K8s 惯例 |
| Docker Compose | 2 空格 | 官方 compose 规范示例 |
| GitHub Actions | 2 空格 | 官方工作流示例 |
| Ansible playbook | 2 空格 | 官方文档 |
| 传统配置文件 | 4 空格 | 与 JSON 美化默认一致 |
对于会被 Kubernetes 或 Docker Compose 消费的文件,使用 2 空格。对于只被人类和自定义工具读取的独立配置文件,两者皆可——只要在同一文件内保持一致。我们的 JSON 转 YAML 转换器默认 2 空格缩进,也可以切换到 4 空格以适应偏好 4 空格的项目。
还有一条规则:子元素必须比父元素缩进更多,但额外的空格数可以是任何正整数(1、2、3、4…)——只要在同一块内保持一致。实践中,始终使用 2 或 4 以保证可读性。
JSON ↔ YAML 中的数字处理
两种格式都支持数字,但边界情况的差异足以在生产环境中引发 bug。
大数字的精度损失
JavaScript 的 Number 类型是 64 位 IEEE 754 浮点数。它能精确表示的整数上限是 2^53 − 1 = 9,007,199,254,740,991。超过这个范围,整数精度会丢失:
// JavaScript 精度损失——这不是 YAML 问题,但会影响 JSON 解析
JSON.parse('{"v": 9007199254740993}').v
// → 9007199254740992 (3 变成了 2——丢失一位)
// 安全——在 2^53 范围内
JSON.parse('{"v": 9007199254740991}').v
// → 9007199254740991 (精确)
这对 JavaScript 环境中的 JSON 转 YAML 转换有影响,因为 YAML 序列化开始之前精度就已经丢失了。Kubernetes 的 metadata.resourceVersion 被设计成字符串字段,正是因为资源版本可能超过安全整数范围。其他看起来像小数字的字段——observedGeneration、uid 组件——相对安全,但 K8s 响应中的任何 int64 字段都存在潜在风险。
应对方案:
- 在处理大数字的转换流水线中使用 Python 或 Go——两者原生支持任意精度整数。
- 在 Node.js 中,使用支持 BigInt 的 JSON 解析器:
JSON.parse(text, (_, v) => typeof v === 'number' && !Number.isSafeInteger(v) ? BigInt(v) : v)。 - 对必须无损往返的字段,在源头将其序列化为字符串。
- 审查转换后的 YAML 时,重点关注
resourceVersion、generation以及时间戳派生的值。
八进制和十六进制的怪癖
YAML 1.1 会将某些数字形状的字符串解析为非十进制整数:
# YAML 1.1 解析的意外
permissions: 0755 # 解析为八进制 493,而非十进制 755
value: 0x1A # 解析为十六进制 26,而非字符串 "0x1A"
# YAML 1.2 行为
permissions: 0755 # 保持整数 755(十进制)——八进制需要 0o 前缀
permissions: 0o755 # 1.1 和 1.2 中均解析为八进制 493
# 两种规范都安全——给任何前导零的值加引号
permissions: "0755" # 永远是字符串 "0755"
八进制陷阱对 Unix 文件权限、带前导零的 IP 地址组件(某些网络设备)、以及任何使用前导零填充的数字代码(邮政编码、产品代码)特别危险。手写 YAML 时务必对这些值加引号,或确保你的转换器会加引号——我们的 JSON 转 YAML 转换器能检测 JSON 中的数字型字符串并保留其字符串类型。
真实转换场景
将 Norway 问题和引号策略应用于真实的转换场景,才能让这些概念变得具体。
Kubernetes 清单从 JSON 转换
经典工作流:kubectl get deploy my-app -o json 给你实时对象的 JSON 格式。你想清理它(删除 status、creationTimestamp、managed fields)并将其作为 YAML 清单提交到 git。
源 JSON(节选):
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "my-app",
"namespace": "production",
"labels": {
"app": "my-app",
"region": "NO"
}
},
"spec": {
"replicas": 3,
"selector": {
"matchLabels": { "app": "my-app" }
},
"template": {
"spec": {
"containers": [{
"name": "app",
"image": "registry.example.com/my-app:v1.2.3",
"env": [
{ "name": "REGION", "value": "NO" },
{ "name": "ENABLE_FEATURE", "value": "yes" }
]
}]
}
}
}
}
期望的 YAML 输出(含 Norway 防护):
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
labels:
app: my-app
region: "NO" # 加引号——Norway 词
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
spec:
containers:
- name: app
image: registry.example.com/my-app:v1.2.3
env:
- name: REGION
value: "NO" # 加引号——Norway 词
- name: ENABLE_FEATURE
value: "yes" # 加引号——Norway 词
注意 replicas: 3 不加引号——它是合法的整数,Kubernetes 也期望它是数字。labels 和 env 值中的 Norway 词被加了引号。不处理 YAML 1.1 布尔值的朴素转换器会静默地产生 region: false 和 value: false。
转换后,用 kubectl apply --dry-run=client -f manifest.yaml 验证。这能在不接触集群的情况下捕获 schema 错误。
在我们的 JSON 转 YAML 转换器中试一试——粘贴上面的 JSON,即刻看到 Norway 安全的输出。用我们的 YAML 转 JSON 转换器验证往返结果。
Docker Compose 从 JSON 转换
CI/CD 流水线有时会从 JSON 配置存储以编程方式生成 Docker Compose 配置,再将其以 YAML 形式写入磁盘供开发者阅读。
关键陷阱——重启策略:
{"restart_policy": "no"}
在 Compose 中,restart_policy: "no" 是有效值,意思是”永不重启容器”。在 YAML 中去掉引号后,这会变成 restart_policy: false,Docker Compose 可能将其视为相同语义(假值 = 不重启)或抛出类型验证错误——不同 Compose 版本的行为不一。引号是必须的。
还需注意: Compose v3 的 deploy.restart_policy.condition: "on-failure"——on-failure 值包含 on 这个词,但有连字符,不在触发词列表中,所以实际上是安全的。然而,condition: on(没有 -failure)会触发误匹配。如果 environment: 块中的环境变量值可能是 Norway 词,也需要加引号。
转换后用 docker-compose config 验证 Compose 文件:它会解析并重新输出规范形式,暴露类型错误。
GitHub Actions 工作流
GitHub Actions 工作流是开发者手动编辑的 YAML 文件。最常见的转换场景是从 GitHub API(返回 JSON)读取工作流数据,转换为本地 YAML 文件进行编辑。
需要重点关注的字段:
# 安全——标准 GitHub Actions 中没有 Norway 触发词
on: # "on" 是这里的 YAML 键,不是值——处理方式不同
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
npm install
npm test
env:
NODE_ENV: production # 安全——不是 Norway 词
DEBUG: "off" # 值中的 Norway 词——需要加引号
注意:on: 作为 YAML 键是特殊的——YAML Norway 问题作用于值,而不是键。但 on 作为值(如 DEBUG: on)会触发强制转换。env: 块尤其值得仔细检查,因为环境变量的值是字符串,但很多是简短的标志位,可能与 Norway 词冲突。
对于包含 shell: 规范的工作流,有效值(bash、pwsh、sh、python)都不会触发 Norway 强制转换。自定义值应该主动加引号。
Terraform JSON 计划 → YAML
terraform show -json tfplan > plan.json 输出 Terraform 计划创建、修改或销毁的内容的详细 JSON 表示。将其转换为 YAML 可以让它在 PR review 和合规审计中更易阅读。
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
# 然后用我们的工具或库进行转换
Terraform 计划 JSON 复杂且层次深。转换时的关键注意事项:
-
大整数 ID。 云资源 ID(AWS 账号 ID、GCP 项目编号)和计算后的属性值可能是大数字。通过 Python 或 Go 转换,以避免 float64 精度损失。
-
版本约束字符串。 Terraform 在 provider 版本约束中使用
~>、>=、<=。这些是字符串值,只要不是 Norway 词,YAML 就能正确处理——~>是安全的。 -
Provider 配置值。 Terraform 计划输出可能包含资源的配置值。如果某个布尔字段默认为
false,而在某个 provider schema 中以"no"表示,在转换为 YAML 时就存在 Norway 风险。 -
.sensitive_values块。 敏感值在计划 JSON 中被脱敏为true布尔值。由于true在任何 YAML 版本中都不是 Norway 触发词,这些值能干净地通过转换。
Terraform 转 YAML 主要用于人工审查,而非重新输入 Terraform。不要将 YAML 清单作为 Terraform 输入——Terraform 的原生格式是 HCL,其 JSON 输入格式是单独规定的。
代码示例——4 种语言
Node.js(eemeli/yaml + js-yaml)
Node.js 生态系统有两个主流 YAML 库,对 Norway 问题的处理方式有实质差异:
// eemeli/yaml——推荐,默认 YAML 1.2,Norway 安全
import { stringify } from 'yaml';
import { readFileSync } from 'fs';
const jsonInput = readFileSync('input.json', 'utf8');
const data = JSON.parse(jsonInput);
// 默认:YAML 1.2——"NO" 保持为 "NO",无布尔值强制转换
const yamlOutput = stringify(data);
console.log(yamlOutput);
// region: NO ← 在 1.2 中安全,但为了最大兼容性可以显式加引号
// 强制 YAML 1.1 行为(用于解析 1.1 的 K8s/Helm 环境)
const yamlForK8s = stringify(data, { version: '1.1' });
// region: 'NO' ← 自动加引号,因为 1.1 会将 NO 解析为 false
console.log(yamlForK8s);
// js-yaml——广泛使用,但 YAML 1.1 语义,不谨慎处理则有 Norway 风险
import yaml from 'js-yaml';
import { readFileSync } from 'fs';
const data = JSON.parse(readFileSync('input.json', 'utf8'));
// 默认 dump——Norway 词可能不被加引号
const unsafe = yaml.dump(data);
// region: NO ← 被 1.1 解析器重新读取时会被解析为 false!
// 更安全:使用自定义 schema 或强制加引号
const safer = yaml.dump(data, {
schema: yaml.JSON_SCHEMA, // 限制为 JSON 兼容类型
noCompatMode: false,
lineWidth: -1,
quotingType: '"',
forceQuotes: false, // 仅在 JSON schema 必要时加引号
});
新项目优先选择 eemeli/yaml。其默认的 YAML 1.2 更安全,Document API 提供细粒度的引号控制,往返保真度也更好。已使用 js-yaml 的项目,使用 JSON_SCHEMA 选项来限制为 JSON 安全的类型。如需在转换前过滤和转换 JSON,可参见 jq 命令行速查手册了解预处理模式。
Python(PyYAML + ruamel.yaml)
Python 是 Kubernetes 工具链、Ansible 和数据工程流水线的主导语言——都是 YAML 的重度用户。
import json
import yaml
import sys
# PyYAML——简单、标准,但默认 YAML 1.1
with open('input.json') as f:
data = json.load(f)
output = yaml.dump(data, default_flow_style=False, allow_unicode=True)
# country: 'NO' ← PyYAML 足够聪明,会自动对 Norway 词加引号
# 但在某些配置下,它不会对 "yes"、"no"(小写)加引号:
# enabled: 'yes' ← 加了引号
# tag: y ← 根据版本可能加也可能不加引号
print(output)
import json
import sys
from ruamel.yaml import YAML
# ruamel.yaml——往返保真度高,支持 YAML 1.2,生产环境推荐
yaml_rt = YAML()
yaml_rt.default_flow_style = False
yaml_rt.width = 4096 # 防止意外换行
yaml_rt.best_map_flow_style = False
with open('input.json') as f:
data = json.load(f)
yaml_rt.dump(data, sys.stdout)
# 保留键顺序,正确处理 Norway 词,往返时支持锚点
对于 Ansible 和 Kubernetes 自动化脚本(将 JSON API 响应转换为 YAML 清单),ruamel.yaml 是更安全的选择。PyYAML 适用于简单脚本——前提是你能控制输入数据并已确认其中不含 Norway 词。
如果你在转换前使用了 JSON5 或 JSONC 配置文件(带注释),需要先剥离扩展语法——参见 JSON5 与 JSONC 格式化指南了解兼容的解析器。
Go(gopkg.in/yaml.v3)
Go 是 Kubernetes 生态系统本身的语言——kubectl、Helm、Argo、Flux 以及大多数 K8s operator 都用 Go 编写。
package main
import (
"encoding/json"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
func main() {
// 读取 JSON 输入
jsonBytes, err := os.ReadFile("input.json")
if err != nil {
panic(err)
}
// 将 JSON 解析为通用 map
var data map[string]interface{}
if err := json.Unmarshal(jsonBytes, &data); err != nil {
panic(err)
}
// 序列化为 YAML——yaml.v3 使用 YAML 1.2 语义
yamlBytes, err := yaml.Marshal(data)
if err != nil {
panic(err)
}
fmt.Println(string(yamlBytes))
// country: "NO" ← yaml.v3 正确地对 Norway 词加引号
// replicas: 3 ← 整数保持整数
// enabled: true ← 布尔值保持布尔值
}
yaml.v3 在 Norway 安全性上相比 yaml.v2 有显著改进。v2 遵循 YAML 1.1,会不加引号地写出 NO;v3 正确地对歧义值加引号。如果你维护的老 Go 项目使用 v2,升级到 v3——API 大体兼容,安全性提升值得这次迁移。
对于使用 Go 结构体(而非 map[string]interface{})进行类型安全转换,使用结构体标签:
type DeploymentLabels struct {
App string `yaml:"app" json:"app"`
Region string `yaml:"region" json:"region"`
}
// v3 中对包含 "NO" 的结构体字段调用 yaml.Marshal 会正确加引号
Bash CLI(yq + jq)
对于 shell 脚本和快速一次性转换,yq(Mike Farah 的版本,mikefarah/yq)可以用单条命令将 JSON 转换为 YAML:
# 安装 yq
brew install yq # macOS
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
chmod +x /usr/local/bin/yq # Linux
# 将 JSON 文件转换为 YAML
yq -P < input.json > output.yaml
# 从 kubectl JSON 输出转换
kubectl get deploy my-app -o json | yq -P > manifest.yaml
# 先用 jq 过滤/转换,再转换为 YAML
kubectl get deploy my-app -o json \
| jq 'del(.status, .metadata.creationTimestamp, .metadata.managedFields)' \
| yq -P > clean-manifest.yaml
jq | yq 管道是强大的模式:用 jq 进行 JSON 操作(过滤字段、重塑结构、查询值),用 yq -P 作为最终的 YAML 序列化器。jq 的详细用法请参见 jq 命令行速查手册,包含 30 个真实模式以及 kubectl 和 aws 集成。
yq 的 Norway 注意事项: yq(mikefarah 版本)遵循 JSON 输入的类型——JSON 中的字符串 "NO" 会被序列化为带引号的 YAML 字符串。但如果你直接用 yq 生成 YAML(而非从 JSON 输入),必须显式对 Norway 词值加引号。使用我们的 YAML 转 JSON 转换器在 yq 输出后验证往返结果。
边界情况与注意事项
除 YAML Norway 问题外,JSON ↔ YAML 转换还有几个会让有经验的工程师踩坑的边界情况:
-
多文档 YAML(
---分隔符)。 单个 YAML 文件可以包含多个用---分隔的文档。JSON 没有等价概念。将多文档 YAML 转换为 JSON 时,大多数工具要么只取第一个文档,要么将所有文档合并成数组,要么报错。将 JSON 转换为 YAML 时,通常会按惯例添加一个---文档头。对于可能遇到多文档文件的流水线,明确记录并规定你的处理行为。 -
YAML 锚点和别名。 YAML 支持
&anchor定义和*alias引用,用于实现 DRY 配置。将 YAML 转换为 JSON 时,锚点必须展开——生成的 JSON 可能比源 YAML 大得多。将 JSON 转换为 YAML 时,转换器无法重建原本不存在的锚点。别名是 YAML 独有的特性。 -
时间戳隐式解析。 YAML 1.1 解析器会将
2024-05-04和2024-05-04T12:00:00Z转换为语言原生的日期对象,而非字符串。当该日期对象被序列化回 JSON 时,输出取决于库:有的输出 ISO 字符串,有的输出 Unix 时间戳,有的输出 null。在未显式对日期加字符串引号("2024-05-04")的情况下往返传递,可能会静默改变格式。 -
!!binary标签。 YAML 可以用!!binary标签嵌入 base64 编码的二进制数据。JSON 没有二进制类型——二进制必须是 base64 字符串。将含!!binary字段的 YAML 转换为 JSON 时,解码为 base64 字符串。转换回来时,如果不了解 schema 就无法重建二进制标签。Kubernetes 在某些 Secret 值中使用!!binary。 -
键类型冲突。 JSON 要求对象键是字符串。YAML 允许任何类型的键——整数键、布尔键,甚至复杂对象键。键为
true: value或1: value的 YAML 文件无法被忠实地表示为 JSON。大多数转换器会将键字符串化,但语义会改变。 -
null 表示的多样性。 在 YAML 中,
null、~、Null、NULL以及空值都表示 null。在 JSON 中,只有null是 null。将 YAML 转换为 JSON 时,这些都会规范化为null。但将 JSON 转换回 YAML 时,null 的表示方式很重要——~更紧凑,null更明确。选一种并坚持使用。 -
键排序变化。 JSON 对象在技术上没有规定键的顺序(尽管大多数解析器保留插入顺序)。YAML 映射同样没有强制顺序要求。但某些 YAML 库在序列化时默认按字母顺序排序键。如果源 JSON 使用了不同的顺序,这可能在版本控制中造成大量 diff。在 PyYAML 中配置
sort_keys=False(仅default_flow_style=False不能防止排序),其他库也有等价选项。
何时不该转换
转换并不总是正确答案。以下场景中,保持原始格式是更好的选择:
如果 YAML 包含记录业务逻辑的注释,不要将 YAML 转换为 JSON。 YAML 注释不在数据模型中——它们会在序列化为 JSON 时消失。如果 Kubernetes 清单有注释解释为何选择特定资源限制,或为何存在安全策略例外,转换为 JSON 会破坏这些文档。保留 YAML。
不要在 CI 流水线中不经往返测试地自动转换配置。 如果你的流水线将 JSON 转换为 YAML 然后应用到集群,加一个往返验证步骤:YAML 转回 JSON,再与原始值比较。这能在类型强制转换的惊喜到达生产环境之前将其捕获。
不要仅仅因为工具输出 JSON 就转换。 kubectl、aws、terraform、docker inspect 都输出 JSON,但这些工具大多数也接受 YAML 作为输入。在构建转换步骤之前,检查目标工具是否可以直接接受 YAML 输入——大多数现代 DevOps 工具都可以。我们的 YAML 转 JSON 转换器最有用的场景是你确实需要 JSON 而目标工具不接受 YAML。
如果 schema 不同,不要转换。 如果你的 JSON 使用 camelCase 键,而 YAML 消费方期望 snake_case(或反之),你除了格式转换还需要一个转换步骤。裸格式转换会产生语法正确但语义错误的 YAML。明确处理 schema 映射。
不要手动保持两种格式同步。 如果你同时维护 config.json 和本应等价的 config.yaml,它们会出现偏差。选定一种规范格式,自动导出另一种——或者更好,选一种格式并消除重复。
FAQ
YAML Norway 问题现在仍影响现代系统吗?
是的——它在生态系统中广泛存在。Kubernetes 和 Helm 在相当大的代码库部分使用 Go 的 yaml.v2 库(YAML 1.1 语义)。Ansible 使用 PyYAML(YAML 1.1)。GitHub Actions 工作流由 GitHub 内部的 YAML 解析器处理,有其自己的行为。实际上,绝大多数 CI/CD YAML 文件都由 YAML 1.1 解析器处理。除非你已明确验证,否则假定使用 1.1 语义。
如果 YAML 更难解析,为什么还要将 JSON 转换为 YAML?
转换不是关于解析难度——而是关于人类可编辑性。JSON 更适合机器;YAML 更适合需要读取、编辑和审查配置文件的人类。提交到 git、在 PR 中审查、由工程师手动调整的 Kubernetes 清单应该是 YAML。同一个清单从 API 获取用于程序化处理时应该是 JSON。我们的 JSON 转 YAML 转换器架起了这座桥梁。
JSON ↔ YAML 可以无损往返吗?
有条件地可以——对于 JSON 兼容的数据。JSON 是 YAML 1.2 的子集,所以任何有效的 JSON 文档都是有效的 YAML 1.2。JSON → YAML → JSON 对任何没有隐式类型强制转换的数据都应该是无损的。Norway 问题意味着 JSON 字符串 "NO" 只有在转换器加了引号的情况下才能在正向传递中存活,然后只有在 YAML 解析器尊重引号的情况下才能在返回传递中存活。两个方向都使用 YAML 1.2 库以保证无损往返。
生产环境最安全的 YAML 库是什么?
Python:配置为 YAML 1.2 的 ruamel.yaml。Node.js:eemeli/yaml(npm 上的 yaml 包)。Go:gopkg.in/yaml.v3。这三者都实现了 YAML 1.2 语义或有明确的 YAML 1.2 模式,能正确处理 Norway 词。新项目避免使用 YAML 1.1 库。如果因兼容性原因必须使用 1.1 库(PyYAML、js-yaml、yaml.v2),始终显式对 Norway 风险字符串加引号。
Kubernetes 清单 YAML 在 JSON 转换后支持注释吗?
不支持——注释无法从 JSON 中恢复。JSON 没有注释语法,所以没有什么可以转换的。运行 kubectl get deploy -o json 并将输出转换为 YAML 用于 git 存储时,生成的 YAML 不会有任何注释。Kubernetes 清单中的注释必须在转换后由人类手动添加。这也是为什么保留手工撰写的 YAML 作为规范来源,往往比通过 JSON API 往返更可取。
如何处理 resourceVersion 或纳秒级时间戳等大整数?
Kubernetes 的 metadata.resourceVersion 刻意设计为字符串字段——Kubernetes 团队清楚地知道 JavaScript 和其他基于 float64 的运行时中的 JSON 解析器会对大整数丢失精度。始终将其视为字符串。对于真正的大数字整数(如某些追踪系统中的纳秒级 epoch 时间戳),使用 Python 的 int 类型、Go 的 int64,或 Node.js 的 BigInt 进行解析。不要在 JavaScript 中不使用自定义 reviver 函数的情况下通过 JSON.parse() 处理它们。转换为 YAML 时,这些大整数是安全的——YAML 对整数没有精度限制。危险在于通过 JavaScript 的 JSON 解析器往返。
YAML 1.2 已被广泛采用了吗?
参差不齐。主要语言库已在迁移:Go 的 yaml.v3、Python 的 ruamel.yaml 和 Node.js 的 eemeli/yaml 都支持或默认使用 YAML 1.2。但 Kubernetes、Ansible 和 DevOps 生态系统的很大部分,由于迁移的向后兼容代价,仍在运行 YAML 1.1 解析器。新项目推荐使用 YAML 1.2,但对于任何你没有自己配置的系统,假定使用 1.1。
团队应该在配置上标准化 JSON 还是 YAML?
按用途标准化,而非按格式。代码消费的配置(API 请求体、SDK 配置文件、程序化工具)用 JSON。人类消费的配置(Kubernetes 清单、CI 流水线、部署配置、Ansible playbook)用 YAML。避免对同一配置混用两种格式——每种配置类型选一种表示,必要时自动化转换。需要转换时,我们的 JSON 转 YAML 和 YAML 转 JSON 转换器完全在浏览器中运行——数据不会离开你的设备。
立即试用
准备好转换真实文件了吗?试用我们的 JSON 转 YAML 转换器——将 JSON 安全地转换为 Kubernetes YAML,自动对 Norway 词(NO、yes、on、off 以及完整的 YAML 1.1 布尔词汇表)加引号,并支持 2 空格或 4 空格缩进。反向转换请使用 YAML 转 JSON 转换器,支持锚点、别名和多文档 YAML。两个工具均完全在浏览器中运行——你的数据绝不离开设备,这在处理包含敏感资源配置的生产 Kubernetes 清单或 Terraform 计划时尤为重要。