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、要可重現的一律用命令列,並把校驗步驟一起寫進腳本。