JSON 转 YAML 命令行:yq 一条命令背后的四个坑
结论:教程里出现频率最高的 yq -P input.json > output.yaml,实测输出的仍然是 JSON。本文用 yq v4.44.6、PyYAML 6.0.3 与 js-yaml 在 2.39 MB 样本上跑出四组数据,讲清输出格式由扩展名决定这件事,并给出批量转换、引号、缩进与大整数精度的正确写法。

结论:中英文教程里出现频率最高的 yq -P input.json > output.yaml,在 yq v4 上产出的仍然是 JSON。想拿 YAML 必须显式写 -o=yaml(或 -oy);-P 只是 --prettyPrint 的缩写,它管缩进和块样式,不管输出格式。
把 JSON 转成 YAML 从来不是「换个文件后缀」这么简单。真正的代价藏在四个地方:输出格式由输入扩展名决定、引号规则会静默改写你的字符串、序列缩进会影响下游工具、大整数一旦经过 yq 就永久掉精度。
一、先把那条最流行的命令跑一遍
在一台干净的机器上装好 yq v4.44.6,建一个最小输入:
printf '{"a":1,"b":{"c":2}}' > s.json
yq -P '.' s.json
得到的不是 YAML,而是带缩进的 JSON:
{
"a": 1,
"b": {
"c": 2
}
}
如果你照着教程把这条命令重定向进 output.yaml,你会得到一个后缀是 .yaml、内容是 JSON 的文件。它能被所有 YAML 1.2 解析器正常读取(YAML 1.2 里 JSON 是子集,这层包含关系在JSON 与 YAML、XML 的互转边界里有完整推导),所以短时间内不会报错——直到有一天有人往里面加注释或锚点,或者 CI 里的 yamllint 报出一堆 truthy value should be one of 才开始查。
真正产出 YAML 的写法是显式指定输出格式:
yq -o=yaml '.' s.json
# a: 1
# b:
# c: 2
二、为什么会这样:扩展名决定输出格式
yq v4 的行为是「输入格式决定输出格式」。它根据输入文件的扩展名推断格式,没有 -o 时沿用推断结果。.json 输入 → JSON 输出;.yaml 输入 → YAML 输出;从 stdin 读且没有扩展名时,默认走 YAML。
同一份输入,四种写法的实测结果:
| 命令 | 输入来源 | 实际输出格式 | 结果 |
|---|---|---|---|
yq -P '.' s.json |
s.json 文件 |
JSON(带缩进) | 落进 .yaml 是坑 |
yq -o=yaml '.' s.json |
s.json 文件 |
YAML(块样式) | 正确 |
yq -P -o=yaml '.' s.json |
s.json 文件 |
YAML(块样式) | 正确,-P 在此多余 |
cat s.json | yq -P '.' |
stdin,无扩展名 | YAML(块样式) | 正确,因为默认 yaml |
第三行说明一件事:-P 在已经有 -o=yaml 的情况下并不总是必要,但它也不是无害的——见第六节的耗时数据。第四行解释了为什么「我照着抄就能用」:很多人是从管道里喂数据的,恰好绕开了扩展名这个分支。
三、批量转换:三种 shell 写法
单个文件转换只是起点,真正需要在命令行里做这件事的场景几乎都是批量的:把一个目录下的所有配置、所有接口快照一次性换成 YAML。
bash / zsh 循环(最直观,适合一个目录):
for f in configs/*.json; do
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
find 递归(子目录也要转时):
find . -name '*.json' -exec sh -c 'yq -o=yaml "." "$1" > "${1%.json}.yaml"' _ {} \;
PowerShell(Windows CI 上):
Get-ChildItem *.json | ForEach-Object { yq -o=yaml '.' $_.FullName > ($_.BaseName + '.yaml') }
三种写法有一个共同的硬性前提:先确认源 JSON 是合法的。转换器和解析器不同,很多转换器在遇到语法错误时只会给出一个含糊的位置,不会像校验器那样把行号列清楚。批量循环里加一道前置校验,成本几乎为零:
for f in configs/*.json; do
jq empty "$f" || { echo "invalid: $f"; continue; }
yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done
jq empty 只做解析不输出,非法 JSON 会返回非零退出码,配合 set -euo pipefail 就能让整条流水线在第一个坏文件处停住,而不是产出一堆半成品。
四、四个输出参数各管一件事
yq 的参数表很长,但 JSON 转 YAML 这条路上真正需要记的只有四个:
| 参数 | 作用 | 什么时候必须写 |
|---|---|---|
-o=yaml / -oy |
指定输出格式为 YAML | 输入是 .json 文件时必写 |
-P / --prettyPrint |
强制块样式并规范缩进 | 从 stdin 读、或要求序列缩进时 |
-I=n |
缩进空格数,默认 2 | 下游要求 4 空格(如部分 Ansible 规范) |
--split-exp |
数组元素拆成多文档 | 生成 Kubernetes 多资源清单时 |
多文档那条值得单独说。Kubernetes 的清单文件习惯用 --- 分隔多个资源,从 JSON 数组生成时:
yq -P -o=yaml '.[] | splitDoc' --split-exp 'true' kubernetes-list.json
不加 --split-exp,你会得到一个 YAML 序列;加了,每个数组元素变成一个用 --- 分隔的独立文档。这两个文件 kubectl apply 都能吃,但 diff 出来的可读性差一个量级。
五、引号:六类必须加引号的字符串
YAML 的类型推断是隐式的,一个字符串值如果长得像数字或布尔,不加引号就会被解析成别的类型。这不是理论风险——实测下列六类值在 yq v4.44.6 上会被自动加引号,说明解析器确实会把它们认错:
printf '{"version":"1.0","enabled":"true","port":"8080","empty":"","colon":"host: value","hash":"v # c","unicode":"端口 8080","ts":"2026-11-03"}' | yq -P -o=yaml '.'
version: "1.0"
enabled: "true"
port: "8080"
empty: ""
colon: 'host: value'
hash: 'v # c'
unicode: 端口 8080
ts: "2026-11-03"
逐个说后果:
| 原值(JSON 字符串) | 不加引号会被读成 | 实际影响 |
|---|---|---|
"1.0" |
浮点 1.0 |
版本号变小数,1.10 会变成 1.1 |
"true" |
布尔 true |
条件判断逻辑反转或类型不匹配 |
"8080" |
整数 8080 |
端口类型变化,字符串比较失败 |
"" |
null |
空字符串消失,下游拿到 None |
"host: value" |
解析错误或嵌套映射 | 冒号后必须有空格,否则语法错 |
"2026-11-03" |
日期对象 | 反序列成 datetime.date 而不是字符串 |
最后一条最阴险。JSON 里日期就是字符串,转过去之后变成 YAML 的日期类型,再转回 JSON 时很多实现会输出 ISO 格式的日期对象而不是原字符串,往返一致性就断了。PyYAML 与 yq 都选择给这些值补引号,等于承认了这一点。
顺带一提,unicode: 端口 8080 这一行没有加引号——中文在 YAML 里是安全的,需要担心的不是它,而是下一节要讲的键顺序与精度。
六、Python 与 Node 的等价写法与实测耗时
不是每台机器都能装 yq(离线环境、受限 CI 镜像、Windows 无包管理器)。Python 与 Node 的等价写法各有各的默认陷阱。三条路线怎么按场景选,JSON 命令行工具与编辑器选型里有一份完整对照表,本文只聚焦 JSON → YAML 这一个方向。
Python + PyYAML(三个参数缺一不可):
import json, yaml
with open("config.json", encoding="utf-8") as f:
data = json.load(f)
with open("config.yaml", "w", encoding="utf-8") as f:
yaml.safe_dump(
data, f,
default_flow_style=False, # 块样式,不是 {a: 1, b: 2}
allow_unicode=True, # 中文不转义成 \u7AEF\u53E3
sort_keys=False, # 保留 JSON 原有键序
)
这三个参数默认是 False / False / True,也就是说开箱即用的 PyYAML 会把键按字母重排、把中文转成 \uXXXX。实测同一份输入,默认参数下输出是:
apiVersion: v1
data:
LOG_LEVEL: info
colon: 'host: value'
unicode: "\u7AEF\u53E3 8080"
kind: ConfigMap
metadata:
labels:
app: web
name: app-config
注意 kind 排到了 apiVersion 后面、metadata 之前——Kubernetes 清单里 apiVersion 必须在 kind 之前是社区约定,键序被打乱虽然不影响解析,但会让 review 的人很难受。
Node + js-yaml:
const fs = require("fs");
const yaml = require("js-yaml");
const data = JSON.parse(fs.readFileSync("config.json", "utf8"));
fs.writeFileSync(
"config.yaml",
yaml.dump(data, { lineWidth: -1, noRefs: true, sortKeys: false })
);
lineWidth: -1 关掉长行折行(否则长 URL 会被从中间断开),noRefs: true 禁止生成锚点与别名(下游是 kubectl 时必须关,部分工具不认锚点)。
实测耗时(样本:2.39 MB JSON、12000 条记录、每条含嵌套对象与两个数组,机器为本次任务所用沙箱环境):
| 实现 | 转换耗时 | 反向解析耗时 | 往返一致性 |
|---|---|---|---|
yq -o=yaml v4.44.6 |
0.45 s | 0.45 s | 一致 |
yq -P -o=yaml v4.44.6 |
4.16 s | — | 一致 |
| Node js-yaml 4.x | 0.23 s | 0.11 s | 一致 |
| Python PyYAML 6.0.3 | 3.26 s | 8.01 s | 一致 |
两个反直觉的点。第一,加了 -P 之后 yq 慢了约 9 倍(0.45 s → 4.16 s),而在这个样本上输出字节数完全相同(都是 2,326,895 字节)——-P 做的是一遍完整的重新排版,代价不小,批量转换几百个文件时能省则省。第二,PyYAML 的反向解析(8.01 s)比正向转换(3.26 s)还慢一倍多,如果你的流水线要反复读回 YAML,Python 不是好选择。
另外,2.39 MB 的 JSON 转成块样式 YAML 后是 2.33 MB,反而小了约 3%。网上流传的「YAML 一定比 JSON 大」并不成立——JSON 的引号、逗号、花括号在深层嵌套时占的体积,可能超过 YAML 的缩进空白。
七、转过去就回不来的三样东西
前三节讲的是「怎么转对」,这一节讲「哪些东西转过去就丢了」。用一份专门构造的输入实测:
printf '{"order_id":12345678901234567890,"zip":"0150","ts":"2026-11-03","f":1.0e6}' > t.json
yq -o=yaml '.' t.json
yq 的输出:
order_id: 1.2345678901234567e+19
zip: "0150"
ts: "2026-11-03"
f: 1000000
再转回 JSON:{"order_id":12345678901234567000,"zip":"0150","ts":"2026-11-03","f":1000000}。
20 位订单号 12345678901234567890 变成了 12345678901234567000,末三位归零,且没有任何警告。 原因是 yq 走 Go 的 float64 解析路径,超过 2^53 的整数无法精确表示,于是改用科学计数法输出,回来时已经回不去了。
同样的输入交给 PyYAML:
order_id: 12345678901234567890
zip: '0150'
ts: '2026-11-03'
f: 1000000.0
Python 的整数是任意精度的,20 位数字完整保留,回读也是 12345678901234567890。如果你要转的数据里有订单号、雪花 ID、身份证这类长数字,别用 yq,用 Python 或先把字段改成字符串。 这一类精度问题的完整机制在JSON 大整数为什么会丢精度里有详细说明。
第二样丢的是 float 与 int 的区分:1.0e6 在 yq 输出里变成 1000000(整数),PyYAML 保留 1000000.0。第三样是键序——PyYAML 默认重排、yq 与 js-yaml 默认保留,跨语言协作时这是个隐藏的分歧源。
八、CI 里别只 convert:校验 → 转换 → lint
把转换当成唯一一步是最常见的流水线缺陷。转换成功退出码为 0,只说明「输入的 JSON 能解析」,不说明输出是对的、也不说明输出符合下游要求。正确的三段式:
#!/usr/bin/env bash
set -euo pipefail
# 1. 校验源:语法层
jq empty deployment.json
# 2. 转换:显式指定输出格式
yq -o=yaml '.' deployment.json > deployment.yaml
# 3. 校验产物:能被 YAML 解析器读回,且符合规范
yq -o=json -I=0 '.' deployment.yaml > /dev/null
yamllint deployment.yaml
第 3 步里那个 yq -o=json ... > /dev/null 看着多余,实际是整段里最有用的一行:它强制把产物重新解析一遍,缩进错误、锚点解析失败、类型被改写在这一步会暴露。跳过它,问题会推迟到 kubectl apply 或应用启动时才炸。
如果产物是给人维护的配置(Helm values、docker-compose、GitHub Actions 工作流),还要额外确认三件事:键序有没有被打乱、长数字有没有掉精度、注释是不是需要人工补——JSON 没有注释,任何注解都只能在转换之后加,转换器不会替你生成。
常见问题
yq -P 和 -o=yaml 到底该写哪个?
-o=yaml 决定「输出是不是 YAML」,-P 决定「YAML 长什么样」。输入是 .json 文件时 -o=yaml 是必需的,-P 只在你需要强制块样式或序列缩进时加。从 stdin 读且不带扩展名时,yq 默认输出 YAML,此时 -P 单独使用就够了。
转换后的 YAML 可以直接给 Kubernetes 用吗?
语法上可以,但有三个前提:确认 apiVersion 之类有顺序约定的键没被重排(PyYAML 默认会重排,要设 sort_keys=False)、确认没有生成锚点别名(js-yaml 要设 noRefs: true)、确认长数字没掉精度。多资源清单还要用 --split-exp 拆成 --- 分隔的文档。
为什么我转出来的 YAML 是一行 {a: 1, b: 2},不是缩进的?
这是流样式,仍然合法,但等于没转。用 -P 强制块样式;如果你用的是 PyYAML,确认 default_flow_style=False(PyYAML 5.1 起默认值已改为 False,老版本需要显式写)。
20 位订单号转 YAML 后末几位变成 0,怎么救?
救不回来,只能预防。yq 走 float64 解析,超过 2^53 的整数必然掉精度。要么改用 Python(整数任意精度,实测完整保留),要么在 JSON 里就把长数字存成字符串——后者更稳妥,因为它同时解决了 JavaScript 侧 JSON.parse 的同类问题。
在线转换工具和命令行怎么选?
涉密配置、带 token 的接口快照用本地命令行或纯前端工具,别上传;一次性、小体积、要可视化确认的用在线工具更快,本站的 /convert/yaml 就在浏览器里跑,数据不出本机。批量、要进 CI、要可复现的一律用命令行,并把校验步骤一起写进脚本。