YAML vs JSON vs TOML: 設定ファイルのフォーマット選び
なぜ設定ファイルのフォーマット選びが、実は重要なのか
もし設定ファイルのフォーマット選びを間違えたら、コンマ一つが見つからずに何時間もデバッグする羽目になります。キーがいつの間にか消えてしまうパーサーと格闘することになるでしょう。新しいチームメンバーに、またあの難解なインデントルールを説明しなければならなくなります。これは仮説ではありません。毎週のように、プロジェクトの活力をじわじわと奪っていく、小さな切り傷のような現実なのです。 現代のソフトウェア開発では、ほぼ必ずYAML、JSON、TOMLのいずれかに遭遇します。Docker ComposeはYAMLを使い、REST APIは圧倒的にJSONでやり取りし、RustのパッケージマネージャーであるCargoはTOMLを選びました。これらの選択はそれぞれ、人間にとっての書きやすさ、機械にとってのパースしやすさ、そして表現力との間の意図的なトレードオフを反映しています。メンテナンスしやすいプロジェクトと、脆いプロジェクトの違いは、最初のチュートリアルで使われていたフォーマットをただ真似るのではなく、そうしたトレードオフを理解しているかどうかにかかっていることが多いのです。 この記事では、この3つのフォーマットを、実際に重要となる観点——構文、データ型、コメント、複数行文字列、そしてツール——にわたって比較します。また、なぜフォーマット間の変換が必要になるのか、そしてさらに重要なことに、その変換プロセスが持つ本当の限界についても解説します。始める前に、それを知っておく方が良いでしょう。
JSON: 厳格でポータブル、でも手書きは驚くほど苦痛
JSON、すなわちJavaScript Object Notationの最大の特徴は、その厳格さです。2017年にRFC 8259として標準化されましたが、その魂は2000年代初頭のダグラス・クロックフォードによる最初の仕様に由来します。どんなデータ構造でも、書き方はただ一つです。キーはダブルクォートで囲む必要があり、末尾のコンマは禁止。そしてコメントは? 存在しません。少なくとも仕様上は。 機械同士の通信において、その堅さはバグではなく機能です。JSONの予測可能性こそが、REST APIやビルドツールの世界を征服した理由です。あらゆる主要言語に標準ライブラリのパーサーがあり、フォーマットが非常に明確なので、送ったものがそのまま受け取られると確信できます。とにかく、うまく動くのです。 苦痛が訪れるのは、人間がそれを手で書かなければならない瞬間です。巨大な`webpack.config.js`ファイルの中で、見つからないコンマを探して10分も費やしたことがある人なら、この気持ちがわかるでしょう。シンプルなデータベース接続ブロックなら問題ありません。 ```json { "database": { "host": "localhost", "port": 5432, "name": "myapp_production" } } ``` しかし、これを40個のキーにスケールアップし、3階層ネストさせて、コンマを一つ忘れたらどうでしょう? パーサーはしばしば諦めてしまい、問題のある行ではなく、ファイルの末尾を指して謎のエラーを吐き出すだけです。コメントがないのも同じくらい厄介です。人々は`"_comment": "この設定はキャッシュのTTLを制御します"`のようなハックに頼り、未来の自分へのメモを残すためだけにデータモデルを汚染しています。 JSON5やJSONC(JSON with Comments)のようなフォーマットが存在するのはこのためです。これらは人気があり、VS Codeの`settings.json`はJSONCを使っていますが、根本的に非標準です。これらが安全なデフォルトだと勘違いしてはいけません。JSONCファイルを標準のパーサーに送れば、クラッシュするでしょう。
YAML: 最高の可読性と、最高に危険な落とし穴
YAML Ain't Markup Languageの略であるYAMLは、人間にとっての読みやすさを最優先します。JSONの波括弧やクォートを捨て去り、クリーンなインデントベースの構造を採用しています。ネイティブなコメント(`#`)があり、複数行の文字列も優雅に扱えます。YAMLで書かれたGitHub ActionsのワークフローやKubernetesのマニフェストは、JSONで書かれたものよりも人間がざっと目を通しやすいのは間違いありません。 先ほどのデータベース設定がYAMLだとこうなります。 ```yaml database: host: localhost port: 5432 name: myapp_production # レプリカセットを2024-03-10に追加 replicas: - replica1.internal - replica2.internal ``` よりクリーンで、変更を説明する重要なコメントが第一級市民として扱われています。複数行文字列もブロックスカラー(`|`や`>`)を使って解決されており、SQLクエリやシェルスクリプトを設定ファイルに埋め込んでも、読めないごちゃごちゃになることはありません。 しかし、この読みやすさには代償が伴い、それは曖昧さと隠れた危険という形で支払われます。これらが悪名高いYAMLの「落とし穴」です。最も有名なのは「ノルウェー国旗問題」で、国コード`NO`が古いYAMLバージョン(多くのツールがまだ使っています!)ではデフォルトでブール値の`false`として解釈されてしまいます。インデントのスペースが一つずれてもエラーにはならず、データの構造全体が静かに変わってしまいます。仕様書自体が86ページもの長さで、これはHTTP/1.1の仕様書よりも長いのです!この事実が、一見シンプルに見える表面の下にどれほどの複雑さが隠れているかを物語っています。 では、どうすればいいのでしょうか? Infrastructure-as-CodeやCI/CDパイプラインのように、設定ファイルが一日中人間によって書かれ、レビューされるような場面では、YAMLの読みやすさはリスクを冒す価値があることが多いです。しかし、コードによって生成され、めったに手で触れられない設定ファイルの場合、その利点は消え去り、手には危険な武器だけが残されることになります。
TOML: 実用的な落としどころ
TOML、すなわちTom's Obvious, Minimal Languageは、実用的な選択肢です。GitHubの共同創設者であるトム・プレストン・ワーナーによって作られ、2021年にバージョン1.0.0として完成したこのフォーマットは、読みやすく、かつ明白で曖昧さのない設定ファイルであることを存在理由としています。 見た目は昔ながらのINIファイルに少し似ており、セクションヘッダーが角括弧で囲まれていますが、明確なデータ型が追加されているのが最大の特長です。この構造は、よりフラットな設定を促し、それは健全な制約となり得ます。 ```toml [database] host = "localhost" port = 5432 name = "myapp_production" # レプリカセットを2024-03-10に追加 replicas = ["replica1.internal", "replica2.internal"] ``` ここでTOMLは、YAMLの最大の問題点に直接答えています。曖昧な裸の文字列はありません。`port = 5432`は整数、`enabled = true`はブール値です。`NO`はただの文字列「NO」です。値が意図しない何かに魔法のように型変換されてしまう心配は一切ありません。また、RFC 3339タイムスタンプを含む日時型を第一級市民としてサポートしており、これは日付文字列の扱いに苦労したことのある人にとっては大きな救いです。 主な弱点は、深くネストされたデータや、繰り返しが多いデータの表現です。テーブルの配列のための`[[section]]`構文は、Cargoの`[[bin]]`エントリのような単純なケースでは機能しますが、すぐに扱いにくくなります。3層や4層のネストが必要な場合、YAMLのインデントモデルが急に魅力的に見えてきます。また、TOMLはYAMLのアンカーやエイリアスのような機能を意図的に省略しているので、環境間で重複を減らす必要がある場合は、お手上げです。大きなTOMLファイルは非常に冗長になりがちです。 採用は進んでいますが、普遍的ではありません。Pythonは3.11で標準ライブラリに`tomllib`を追加し、これは大きな信任投票となりました。それ以前は、サードパーティのパッケージを入手する必要がありました。もしあなたのチームがRust、モダンなPython、あるいはGoのエコシステムに深く関わっているなら、TOMLは優れた快適な選択肢です。多くの異なる言語にまたがるプロジェクトの場合は、採用を決める前にまずパーサーが利用可能かを確認した方が良いでしょう。
直接対決: 機能比較表
すべてのトレードオフを一つの場所にまとめてみましょう。ここでは、実世界のコンフィグファイルで最も頭痛の種となる機能について直接比較します。 **コメント:** YAML & TOML: はい、`#`による行コメントが使えます。JSON: いいえ。仕様にはなく、今後もありません。諦めるしかありません。 **末尾のコンマ:** YAML & TOML: 主な区切り文字としてコンマを使わないため、該当しません。JSON: 禁止。gitの差分を厄介にし、パースエラーを引き起こす典型的な原因です。 **複数行文字列:** YAML: ブロックスカラー(`|`, `>`)による優れたサポートがありますが、末尾の改行を制御するインジケーター(`-`, `+`)は少しトリッキーかもしれません。TOML: トリプルクォート文字列による良好なサポートがあります。JSON: 最悪です。手動で`\n`エスケープを追加するしかありません。醜く、エラーも起きやすいです。 **日時型:** TOML: はい、第一級の日時オブジェクトがあります。YAML: はい、バージョン1.2で対応していますが、パーサーのサポートは一貫していないことがあります。JSON: いいえ。日付は慣習的にただの文字列であり、それをパースするかどうかはあなた次第です。 **アンカーとエイリアス(DRYな設定):** YAML: はい、`&anchor`と`*alias`で可能です。繰り返しを減らすための強力ですが、しばしば混乱を招く機能です。TOML & JSON: いいえ。自分で繰り返すか、前処理のステップに頼るしかありません。 **スキーマ検証:** JSON: はい、成熟し広くサポートされている標準であるJSON Schemaがあります。YAML: JSON Schemaを使えますが、`ajv`や`yamale`のような特定のツールが必要です。TOML: こちらのツールはまだ成熟していません。明確な弱点です。 **同等のデータに対するファイルサイズ:** minifyすればJSONが最もコンパクトです。人間が読むファイルの場合、YAMLとTOMLは同程度です。典型的な設定ファイルでその差が10-15%を超えることは稀なので、これは主要な決定要因ではありません。 **パース速度:** アプリ起動時に設定ファイルを読み込む程度なら、どれも十分に高速です。高スループットのシリアライゼーション(毎秒数千オペレーション)では、`simdjson`のような高度に最適化されたライブラリのおかげで、JSONが誰もが認めるチャンピオンです。
フォーマット間の変換: できることと、できないこと
遅かれ早かれ、これらのフォーマット間で変換する必要が出てくるでしょう。プロジェクトを移行したり、JSON APIからYAML設定を生成したり、JSONしか受け付けないレガシーなツールにTOMLファイルを渡したり。よくあることです。 CocoConvertは、JSONからYAML、YAMLからJSON、TOMLからJSON、そしてJSONからTOMLへのコンバーターで、その機械的な作業を代行できます。基本的なデータ型を持つシンプルな設定ファイルなら、変換はロスレスで高速です。Convertページでファイルをアップロードし、ターゲットフォーマットを選んで実行するだけです。 しかし、自動変換には厳然たる限界があります。翻訳の過程で何が失われるかを認識しておく必要があります。なぜなら、それはツールのバグではなく、フォーマット間の根本的な不一致だからです。 **JSONへの変換ではコメントが失われる。** JSONにはコメントという概念が一切ありません。コメント付きのYAMLやTOMLファイルをJSONに変換すると、それらのコメントは永遠に失われます。回避策はなく、これはJSON仕様自体の制約です。 **YAMLのアンカーは展開され、保持されない。** YAMLファイルがDRYを保つために`&defaults`や`*defaults`を使っている場合、その構造は失われます。コンバーターはアンカーを展開するため、出力されるデータは同一ですが、至る所で重複することになります。結果のファイルは機能しますが、メンテナンス性は低下します。 **TOMLの日時型は文字列になる可能性がある。** TOMLのネイティブな`created_at = 2024-01-15T09:30:00Z`は、JSONに直接の相当物がありません。CocoConvertはこれを標準的なISO 8601文字列に変換します。これは正しい慣習です。しかし、そのJSONを読み込むアプリケーションは、その文字列を日付オブジェクトにパースし直すべきだと知っている必要があります。 **深くネストしたTOMLからYAMLへの変換**は構造的には問題なく動作しますが、出力は意外なものになることがあります。TOMLの`[[array of tables]]`構文は、YAMLのマッピングのシーケンスに対応しますが、見慣れていないと奇妙に見えるかもしれません。 本番環境で信頼する前に、必ず変換後の出力をレビューしてください。変換して元に戻す(ラウンドトリップ)したファイルに対して素早く`diff`を取ることが、予期せぬ問題を発見する最善の方法です。
最終判断: 状況別のフォーマット選び
唯一の正解はありませんが、だからといって選択が重要でないと勘違いしてはいけません。それぞれのフォーマットをいつ使うべきかについては、明確なヒューリスティクスが確立されています。 **JSONを使うべき時:** 設定が人間ではなく、機械向けの場合。ツールによって生成・消費され、人間が手で編集することがほとんどないなら、JSONの厳格さは美徳です。`package.json`や`tsconfig.json`がJSONなのはこのためです。最大の互換性が目標です。 **YAMLを使うべき時:** 人間が主な利用者である場合。Kubernetesのマニフェスト、GitHub Actions、Ansibleのプレイブックなどでは、読みやすさが最優先されます。設定は常に人々によって書かれ、レビューされます。確かに落とし穴は実在しますが、管理可能です。一つだけ譲れない推奨事項があります。もしYAMLを使うなら、CIパイプラインで`yamllint`のようなリンターを*必ず*強制すること。例外はありません。 **TOMLを使うべき時:** YAMLの危険な曖昧さなしに、その読みやすさが欲しい場合。設定がほとんどフラットで、チームが十分なサポートのあるエコシステム(RustのCargo.tomlやモダンなPythonのpyproject.tomlなど)で作業しているなら、TOMLは素晴らしい選択肢です。構文がシンプルでパーサーのエラーも概して親切なので、アプリケーションのエンドユーザーが編集するのにも最もフレンドリーなフォーマットです。 **3つのどれも完璧にフィットしない時:** 一歩引いて、静的な設定ファイルがそもそも適切なツールなのかを問い直してください。単純なキーバリューペアは、環境変数で十分な場合が多いです。条件分岐ロジックを含む本当に複雑なシナリオをYAMLやTOMLに無理やり押し込もうとするのは間違いです。あなたはそれらを卒業したのです。設定には本格的なプログラミング言語が必要だと認める時です。それがDhallやStarlarkのような専用の設定言語であれ、単なるPythonスクリプトであれ。より大きなコミットメントが必要ですが、ネストしたYAMLで怪物を作り上げるよりはマシです。 もしプロジェクトの途中で、これらの判断を誤った側にいることに気づいたら、CocoConvertが移行を手伝ってくれます。しかし、どのフォーマットがあなたのチームに最も貢献するのかという戦略的な選択は、依然としてあなた次第です。