JSON vs YAML vs TOML: どの設定形式を使うか
最も一般的な3つの設定形式の実用的比較とユースケース。
設定形式は、パーサーが no を黙って false に変えたり、64ビットIDを切り詰めたり、設定ファイルから任意のオブジェクトを生成したりするまでは、互いに交換可能に見えます。JSON、YAML、TOMLが現代のツーリングを支配しており、チームが経験する痛みの大半は、それぞれの形式が正確にどこで歪むのかを知らないことから来ています。これは好みの比較ではなく、メカニズムレベルの比較です。
3つの仕様、3つの哲学
JSONはRFC 8259とECMA-404で定義されています。6つの値型、コメントなし、ナプキンに収まる文法。YAML 1.2.2(2021)は、3つの表現レイヤー、タグ、アンカー、マルチドキュメントストリームを記述する約100ページの文書です。TOML 1.0.0(2021)はその中間に位置します。ハッシュテーブルへ曖昧さなくマッピングされるよう明示的に設計された、型付きでコメントに優しい形式です。
仕様のサイズは実務で重要です。主要なJSONパーサーはすべて本質的に同一に動作します。一方、2つのYAMLパーサーは同じファイルについて正当に異なる結果を出し得ます。PyYAMLを含む多くのライブラリがいまだYAML 1.1のセマンティクスを実装し、他は1.2を実装しているからです。
JSON: 予測可能だが数値に鋭いエッジ
{
"port": 8080,
"debug": true,
"hosts": ["a.example.com", "b.example.com"]
}
JSONの本当のリスクは数値と重複キーに潜んでいます。
- 数値精度。 RFC 8259は精度要件を定めていませんが、JavaScriptはすべての数値をIEEE 754倍精度として解析します。2^53 - 1を超える整数は静かに桁を失います。TwitterのAPIが
idとid_strの両方を提供している理由です。64ビット識別子は文字列としてシリアライズしましょう。 - 重複キー。 仕様は名前が一意であるべき(SHOULD)としか言っていません。大半のパーサーは最後の出現を、一部は最初を保持し、バリデーターと実行系が同じ文書を異なって解析するとき、この不一致が実際のセキュリティバイパスに悪用されてきました。
- コメントなしは意図的な設計。 Douglas Crockfordはコメントベースのパーサーディレクティブを防ぐためにコメントを削除しました。手編集する設定にはJSONC(
tsconfig.jsonの実際の形式)かJSON5を使い、どちらを受け入れるかツーリングで明示してください。素のJSONパーサーはどちらも拒否します。
JSONはAPIペイロード、ロックファイル、人間より機械が書く頻度の高いあらゆるものに使いましょう。
YAML: 読みやすいが1.1型の落とし戸
port: 8080
debug: true
hosts:
- a.example.com
- b.example.com
YAML 1.2は no、yes、on、off を普通の文字列にしましたが、YAML 1.1パーサーはこれらをブール値に強制変換します。国コード NO が false になる有名なNorway問題です。1.1のルールは 1:30 を60進整数の90として解析し、バージョン番号 3.10 を浮動小数点の 3.1 に変えます。どのパーサーがファイルを読むか制御できないことが多いため、運用ルールはこうです。他の型のスカラーに見え得るすべての文字列を引用符で囲むこと。
さらに2つのメカニズムに敬意を払うべきです。
- アンカーとエイリアス(
&base/*base)は設定の重複をきれいに排除しますが、billion-laughs攻撃を可能にします。少数のネストしたエイリアスがギガバイト級に展開されます。信頼できないYAMLはエイリアス制限付きで解析するか、エイリアス自体を拒否してください。 - 任意オブジェクトの構築。 PyYAMLのレガシー
yaml.load()は、タグ付きノードから任意のPythonクラスをインスタンス化できました。常にsafe_loadか、デフォルトで安全なパーサーを使いましょう。
インデントにタブは禁止され、空白1つがネストを静かに変え得ることも忘れずに。エコシステムがYAMLを強制するなら(Kubernetes、GitHub Actions、Ansible)、スキーマ検証と組み合わせましょう。yamllintにschemastore.orgのJSON Schemaを加えれば、大半の惨事を本番到達前のCIで捕捉できます。
TOML: 意図的に退屈
port = 8080
debug = true
hosts = ["a.example.com", "b.example.com"]
[database]
url = "postgres://localhost:5432/app"
[[server]]
name = "eu-1"
[[server]]
name = "us-1"
TOMLの設計目標は具体的な保証として現れます。
- すべての値が明示的な型を持ちます。文字列、整数、浮動小数点、ブール、配列、テーブル、そしてRFC 3339の日時。日付が第一級市民なので文字列の慣習は不要です。
- 重複キーは実装の選択ではなく、ハードなパースエラーです。
- インデントにセマンティクスがありません。構造は
[table]ヘッダーとドット記法キーから来るため、再フォーマットが意味を変えることはあり得ません。
コストはネストの人間工学です。深くネストしたテーブルの配列はすぐに冗長になります。Cargoとpyproject.tomlがTOMLで快適に感じられる一方、Kubernetesマニフェストはそうならない理由です。有用なヒューリスティック: 設定が3レベル以上ネストするなら、問題は形式ではなく設定スキーマです。
実務での選択
- 機械間データ、API、ロックファイル: JSON
- 人間が編集するアプリケーション・ツール設定: TOML
- エコシステムが強制するインフラ設定: YAML、必ずスキーマ検証と曖昧なスカラーの引用符付けとともに
- コメントが必要だがJSON互換であるべき設定: JSONC/JSON5、明示的に文書化
繰り返される失敗
- 64ビットIDをJSON数値として出力し、数週間後にデータ破損をデバッグ
- 引用符なしのYAML値: 国コード、偶然すべて数字のgit SHA、
3.10のようなバージョン - エイリアス・深さ制限なしでユーザーからYAMLを受け入れる
- すべてのパーサーが重複キーを同じ方法で処理すると仮定する
- シリアライザーの代わりに文字列連結でこれらの形式を手作りする
フォーマット間の変換
3つの形式はいずれも交差部分で同じデータモデル(マップ、配列、スカラー)を共有するため、機械的な変換は通常一方向にのみ無損失です。JSONからYAMLへは常に機能します。YAMLからJSONへはアンカー、マルチドキュメント、文字列でないキーで失敗します。TOMLからJSONへはコメント層が失われますが、それはしばしばファイルで最も価値のある内容です。人間が編集する形式を信頼できる唯一の情報源とし、機械用の形式はCIで生成し、往復変換は決してしないこと。一方向にのみ変換し、両方の成果物をコミットし、レビューで差分を確認しましょう。
コミット前に検証を。sdk.is/json-formatter のJSONフォーマッターや sdk.is/yaml-validator のYAMLバリデーターに設定を貼り付け、問題はパーサーに先に見つけさせましょう。