JSON vs YAML: いつどちらを使うか、そしてそれぞれの落とし穴
JSON と YAML は同じデータをモデル化しますが、失敗の形は鋭く異なります。YAML の型強制と空白の罠 vs JSON のコメントの欠如と冗長さを整理します。
JSON と YAML は同じ形を記述します。マップ、リスト、スカラーです。そして両者の選択 はたいていスタイルの好みとして語られます。違います。両者は異なる失敗の形を持ち、 その失敗は別々の場所に落ちます。JSON のものは編集時に噛みつき、YAML のものはパース 時に静かに噛みます。この記事は、各形式がどこで真価を発揮するか、並べた仕組み、そして 動いている設定を午前 2 時のインシデントに変える落とし穴を扱います。
関係
YAML 1.2 は実用上、JSON の上位集合です。すべての有効な JSON ドキュメントが有効な YAML ドキュメントでもあります。YAML の仕様が JSON 構文を部分集合として採用したから です。YAML パーサはこれをそのまま読みます。
{"service": "api", "replicas": 3, "ports": [80, 443]}
それは聞こえるより重要です。YAML が JSON のデータモデルをまるごと受け継ぐという ことです。JSON が表現できて YAML ができないものはありません。違いは形式が保持できる ものではなく、人がそれをどう書くか、そしてパーサが YAML の緩い文法が招くあいまいさを どう解釈するか、にあります。
並べて
同じドキュメントを、まず JSON で見るとこうです。
{
"service": "api",
"replicas": 3,
"image": "registry.example.com/api:1.4.0",
"ports": [80, 443],
"env": {
"LOG_LEVEL": "info",
"REGION": "us-east-1"
}
}
そして YAML で見るとこうです。
# api サービス用のデプロイ設定
service: api
replicas: 3
image: registry.example.com/api:1.4.0
ports:
- 80
- 443
env:
LOG_LEVEL: info
REGION: us-east-1
YAML 版は波括弧、角括弧、キーとほとんどの文字列の周りの引用符、カンマを落とし、 コメントを足します。構造は句読点ではなくインデントで運ばれます。設定を手で編集する 人にとって、これは本当に快適です。落とし穴は、その便利さのすべての断片が、パーサが 推測しなければならない場所でもあり、その推測こそ物事が狂う場所だということです。
機能比較
| 機能 | JSON | YAML |
|---|---|---|
| コメント | なし(仕様が禁止) | あり(#) |
| 末尾カンマ | なし(仕様が禁止) | 該当なし(カンマなし) |
| 複数行文字列 | エスケープ \n のみ |
ブロックスカラー(` |
| アンカー / エイリアス | なし | あり(&, *, <<) |
| パースの厳格さ | 高い(単一の文法) | 低い(文脈依存、バージョン依存) |
| 型推論 | なし(明示的) | 積極的(暗黙的) |
| 普遍性 | 普遍 | 広いがむら |
| 攻撃面 | 最小 | アンカー、カスタムタグ、深い入れ子 |
YAML の落とし穴
ここが覚える価値のある部分です。失敗が静かだからです。
ノルウェー問題
YAML 1.1 は素の単語の長いリストを真偽値として扱います。yes、no、on、off、
true、false、y、n です。広くデプロイされた多くのパーサが今も 1.1 の挙動を
既定にします。だからこれは、
country: NO
NO を文字列 "NO"(ノルウェーの国コード、それが名前の由来)ではなく真偽値
false としてパースします。同じ種類のバグです。
version: 1.0 # 浮動小数 1.0 としてパース、末尾の 0 が落ちる
build: 1.20 # 1.2 になる
zip: 02134 # 先頭の 0: 1.1 では 8 進数、または 2134 に剥がれる
git_sha: 1234567 # 全部数字の SHA の接頭辞が整数になる
mac: 12:34:56 # 1.1 がコロンを 60 進数として読む
enabled: off # 文字列 "off" が false になる
郵便番号、git の SHA、バージョン文字列、MAC アドレス、電話番号、シリアルのように、
数字に見えるが意味上は文字列であるものは、すべて静かな型強制の候補です。解は機械的
です。引用符で囲んでください。country: "NO"、version: "1.0"、zip: "02134"。
引用符はパーサのバージョンによらずスカラーを文字列に強制します。その先では、YAML
1.2 /「コアスキーマ」で構成したパーサを選んでください。真偽値の集合を true/false
だけに狭め、60 進数と 8 進数の罠を取り除きます。
意味を持つ空白
インデントが構造なので、インデントの誤りは構造の誤りです。タブは YAML でインデント にまったく許可されません。変換しなかったエディタから来たさまよいのタブはハードな パースエラーで、メッセージはめったに本当の行を指しません。ブロックを別のインデントの 文脈にコピー&ペーストすると意味が変わります。2 つの空白を失ったリスト項目は、静かに かつての親の子ではなく兄弟になります。このどれも、構造が「有効」であることでは捕らえ られません。きちんとパースされ、ただ意図したものと別のドキュメントになるだけです。
アンカー、エイリアス、マージキー
YAML はノードを一度定義して再利用させます。
defaults: &defaults
retries: 3
timeout: 30
prod:
<<: *defaults
timeout: 60
&defaults がマップをアンカーし、*defaults がそれを参照し、<< がそれをマージ
します。これは DRY な設定に本当に便利です。そして、ファイルがそれに重く頼ると可読性
の崖でもあります。キーの実効値が今やドキュメントの別の場所に宿るからです。さらに
悪いことに、再帰的なエイリアスは「billion laughs」サービス拒否攻撃を可能にします。
それぞれが前のものを参照するひと握りの入れ子のアンカーが指数的に膨張し、パース時に
メモリを枯渇させます。膨張を制限しないパーサは脆弱です。信頼できないソースから YAML
を受け取るなら、エイリアス展開の制限があるパーサ(または「safe」ローダー)を使い、
アンカーをまるごと無効にすることを検討してください。
型強制、一般に
貫く主題は、YAML が型を推論して助けようとし、推論は推測である、ということです。防御
はどの場合も同じです。概念的に文字列であるものはすべて引用符で囲み、寛容な既定では
なく厳格な 1.2 のパーサでロードしてください。Python では yaml.load ではなく
yaml.safe_load であり、ほかの生態系ではライブラリが公開するスキーマや厳格モードを
探してください。
JSON の限界
JSON の厳格さは、パース時にほとんどあなたを驚かせない理由です。しかし同じ厳格さが、 それを人が保守するには悪い形式にします。
- コメントなし。 仕様にありません、それだけです。これが JSONC(VS Code の設定)、
JSON5、そして
// コメントのハックが存在する理由です。注釈できない設定は、未来の あなたが安全に変えられない設定です。 - 末尾カンマなし。 配列やオブジェクトに行を足すと、上の行にカンマを足すことを 覚えていなければなりません。これが手で編集する JSON の最もよくある誤りです。
- 手編集に冗長。 波括弧、角括弧、引用符付きのキーは、人が編集者のときノイズ です。設定の規模では積み上がります。
- ネイティブの日付やコメント型なし。 日付は慣習上の文字列で、それについての メタデータを運びません。
このどれも機械対機械のトラフィックには問題になりません。そこでは誰もペイロードを手で 編集しません。人が毎週エディタで開くファイルには甚大に重要です。
いつどちらを使うか
分割は失敗の形に従います。機械がデータを生産・消費し、人がほとんど触れないところでは どこでも JSON を使ってください。API のリクエスト・レスポンスの本文、サービス間の交換、 ログレコード、プログラムで直列化される何でもです。厳格さがそこでは機能です。ちょうど 1 つの解釈が欲しく、スキーマがドキュメントなのでコメントが要りません。
人が手で編集する設定には YAML を使ってください。CI パイプライン、Kubernetes の マニフェスト、Ansible のプレイブック、アプリケーションの設定です。コメント、複数行 文字列、軽い文法が、まさに人がループの中にいるところで真価を発揮します。
居心地の悪い注意点は、YAML の落とし穴がまさにこの場合に最も強く噛みつくことです。YAML
が輝く人編集の設定ファイルが、引用符のない NO、タブ、間違ってインデントされたリスト
項目がレビューをすり抜けて出荷される、まさにそのファイルです。だから YAML を安全に
する規律、つまり文字列らしいスカラーに引用符を付けること、CI でインデントをリント
すること、スキーマで検証することは、YAML が最も得意とするワークロードに対して妥協
できません。
その検証の段階はパイプラインに組み込む価値があります。スキーマが yaml.load が決して
警告しない強制のバグを捕まえます。私たちは
JSON Schema で検証する で仕組みを扱い、それは
いったんパースされればどちらの形式にも働きます。
2 つの形式の間でドキュメントを移すなら、つまり JSON の API フィクスチャを YAML の設定に 移植したり、マニフェストをツールが要求する JSON へ平坦化し直したりするなら、私たちの JSON ⇄ YAML 変換器 が往復を処理して構造を保ち、 JSON フォーマッタ がコミット前に JSON 側をリントして整形します。