你手里有一份 API 返回的 JSON,想让它可读;或者你有一份 YAML 配置,而某个工具只认 JSON。两者互转是个已经解决的问题,但转换并非完全无损,工具也各有锋利的边角。下面这份实操手册:命令、库,以及每一个会悄悄把你的数据重排的陷阱。
命令行:yq、python、jq
Go 版 yq(mikefarah/yq)最适合随手转换:
yq -o=yaml -I=2 data.json > data.yaml # JSON 转 YAML
yq -o=json data.yaml > data.json # YAML 转 JSON
Python 一行命令到处都能用,只要装了 PyYAML:
python -c "import sys,yaml,json; yaml.safe_dump(json.load(sys.stdin), sys.stdout, default_flow_style=False, sort_keys=False)" < data.json
注意 default_flow_style=False——不加的话,PyYAML 可能把嵌套结构压成单行流式写法,比如 {a: 1},而不是块式。sort_keys=False 则保留你的键顺序,而不是按字母重排。
jq 自己吐不出 YAML,但它是转换前重塑 JSON 的正确工具:
jq '.items[] | {name: .name, price: .price}' data.json | yq -o=yaml
代码里的库:Python 与 JavaScript
Python 用 PyYAML:
import yaml, json
with open("data.json") as f:
data = json.load(f)
with open("data.yaml", "w") as f:
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
JavaScript 里 js-yaml 是标准:
const yaml = require("js-yaml");
const fs = require("fs");
const data = JSON.parse(fs.readFileSync("data.json", "utf8"));
fs.writeFileSync("data.yaml", yaml.dump(data, { noRefs: true, lineWidth: -1 }));
noRefs: true 阻止 js-yaml 给重复对象自动造锚点(某些管道里这会让输出变得无法当作普通数据重新解析),lineWidth: -1 关掉它自动换行。
前后对照:一次真实的往返
输入的 JSON:
{
"name": "checkout",
"enabled": true,
"timeout": 30,
"hosts": ["a", "b"]
}
经过 yq -o=yaml 之后:
name: checkout
enabled: true
timeout: 30
hosts:
- a
- b
很干净。下面是陷阱。
陷阱一:长字符串被折行
默认情况下,PyYAML 和 js-yaml 都会在大约 80 列处给长字符串折行。一个长 URL 或 base64 串会突然多出换行,某些期望单行的解析器就会出错。强制关掉:PyYAML 用 width=4096(js-yaml 用 lineWidth),并且始终传 default_flow_style=False 得到可读的块式输出。
陷阱二:null 与空
JSON 的 null 变成 YAML 的 null(或 ~)。但空字符串 "" 和缺失的键是两回事,而 YAML 里"冒号后什么都没有"的值解析回去是 null,不是 ""。如果你的消费方要区分"空字符串"和"不存在",就把空值显式加上引号:field: ""。
陷阱三:数字与科学计数法
大整数和浮点可能以科学计数法回来。1000000000000 这种整数通常能正常往返,但像 1e-7 的浮点可能被重写成 1.0e-07;超出安全范围的 64 位整数在 JavaScript 里还会丢精度。对于 ID 和金额,最好用字符串传递。
陷阱四:含特殊字符的键
JSON 里 "x-foo" 或 "http://example" 这样的键是合法的,但在 YAML 里,以特殊字符开头,或者含有 :、# 的未加引号键,要么报错要么被误解。输出时务必给这类键加引号;好的库会这么做,但手写的转换器常常不会。
陷阱五:yes/no/on/off 布尔陷阱
YAML 把 yes、no、on、off、true、false 统统解析成布尔值。如果某个 JSON 字符串值恰好是 "off",转成 YAML 再转回来,就可能变成布尔值 false。给这些字符串加上引号。这是 JSON 与 YAML 互转里最常见的静默数据损坏。
陷阱六:锚点不会被自动生成
JSON 没有锚点,所以 JSON 转 YAML 不会产生任何锚点。反向走,YAML 的锚点和引用会塌缩成重复的字面量——结构保留,复用没了。别指望转换器会替你"去重" YAML。
陷阱七:注释必然丢失
JSON 本来就没有注释,所以 JSON 转 YAML 只是没加上而已。YAML 转 JSON 则永远丢弃注释。没有任何转换器能让注释往返,因为 JSON 根本装不下它们。
那为什么还要来回转
最有用的理由是调试 API 响应。你从某个服务拿到 200 行的 JSON 响应,粘进转换器,突然就能读了、能加注释、能推敲。改完再转回去喂给 mock。它是可读性之桥,不是存储格式。
大文件与内存
两种格式都要先把整份内容读进内存才会开始处理。一份 2 GB 的 JSON 在默认 Python 进程里不会舒服,而流式读 YAML 又很别扭。大文件请用 ijson 流式读 JSON,或者分块处理;别指望一条 yq 会对内存温柔。
最省事的那条路
一次性转换,JSON ⇄ YAML 转换 在浏览器里就能双向处理,数据不会离开你的机器——当文件里含有密钥或客户数据时,这点尤其重要。任何转换之后,扫一眼布尔值和像版本号的字符串;静默损坏就藏在那里。