JSON 工具箱

JSON 轉 YAML 命令列:yq 一條命令背後的四個坑

結論:教學裡出現頻率最高的 yq -P input.json > output.yaml,實測輸出的仍是 JSON。本文用 yq v4.44.6、PyYAML 6.0.3 與 js-yaml 在 2.39 MB 樣本上跑出四組數據,講清輸出格式由副檔名決定這件事,並給出批次轉換、引號、縮排與大整數精度的正確寫法。

進階約 9 分鐘發佈於 2026-11-03更新於 2026-11-03
終端機裡把 JSON 轉成 YAML 的指令與輸出對比封面

結論:中英文教學裡出現頻率最高的 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、要可重現的一律用命令列,並把校驗步驟一起寫進腳本。