JSON Toolkit

JSON を YAML に変換するコマンドライン:yq の 1 コマンドに潜む四つの落とし穴

要点:教程でもっとも頻出する 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 のため

三行目は一つのことを示しています。-o=yaml がある状況では -P は必ずしも必要ではないということです。しかし無害でもありません——第六節の所要時間データを見てください。四行目は「教程の丸写しでも動いてしまう」理由を説明しています。多くの人はパイプでデータを流し込むため、偶然にも拡張子の分岐をすり抜けているのです。

三、一括変換:三つの shell の書き方

単一ファイルの変換は出発点にすぎません。コマンドラインでこれをやる理由は、実用上ほぼ一括です。あるディレクトリの全設定、取得済みの全 API スナップショットを、一度に 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 が妥当であることを確認すること。変換器はパーサーではありません。構文エラーに遭遇したとき、多くの変換器は曖昧な位置を示すだけで、validator のように行番号をきちんと列挙してくれません。ループの内側に前置チェックを一つ足すコストはほぼゼロです:

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 より前に置くのがコミュニティの慣習です。キー順が乱れてもパースは壊れませんが、レビューする人をひどく苦しめます。

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 です。変換対象に注文番号、Snowflake 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 で --- 区切りの文書に分割してください。

変換結果がインデントされず 1 行の {a: 1, b: 2} になるのはなぜ?

それはフロースタイルです。妥当な YAML ですが、変換した意味がありません。-P でブロックスタイルを強制してください。PyYAML を使っているなら default_flow_style=False を確認してください(PyYAML 5.1 以降は既定値が False になりましたが、古いバージョンでは明示的に書く必要があります)。

20 桁の注文番号が YAML 変換後に末尾数桁が 0 になった、どう取り戻す?

取り戻せません。予防しかありません。yq は float64 で解析するため、2^53 を超える整数は構造上必ず精度を落とします。Python に切り替える(整数は任意精度で、実測では完全に保たれます)か、JSON の段階で長い数字を文字列として保存してください。後者の方が堅牢です。JavaScript 側の JSON.parse における同種の問題も同時に解決するからです。

オンライン変換ツールとコマンドライン、どちらを選ぶべき?

機密を含む設定や token 入りの API スナップショットは、ローカルのコマンドラインか純フロントエンドのツールを使い、アップロードしてはいけません。一度きりで小さく、目視で確認したい場合はオンラインツールの方が速いでしょう。本サイトの /convert/yaml はブラウザ内で完結し、データは端末から出ません。一括処理、CI 組み込み、再現性が必要なものはすべてコマンドラインを使い、検証工程も同じスクリプトに書き込んでください。