DevKitLab Logo DevKitLab
JSON / YAML / TOML / 設定ファイル

JSON・YAML・TOML、設定ファイルはどれを選ぶべきか

小さな設定ファイルを一つ追加しただけの PR が、レビューで四十件のコメントに膨れ上がる——YAML? TOML? いっそ JSON?——そして何も変わらない。この議論は間違った問いで詰まっている。どの形式が一番いいか、ではない。最良など存在せず、どの形式も、最大の長所には見合った短所がついてくる。それが見えれば、選択はずっと単純な二つの問いに収束する。

小さなファイルを一つ——新しいサービスの設定を——追加しただけの PR が、レビューで脱線する。「なぜ YAML? あのインデント規則を誰が覚えていられる」「TOML のほうがすっきりしている、それにしよう」「もう新しい形式を増やすのはやめないか? うちは JSON だらけだ」。四十件のコメントを経ても、ファイルは一文字も変わらず、誰も納得していない。議論は、いつも詰まるその問いで詰まっている——どの形式が一番いいのか

だが本題はこうだ。最良など存在せず、それを問うこと自体が、このスレッドが動かなくなった理由だ。JSON・YAML・TOML は一つの称号を争う三つの候補ではない。それぞれ違う仕事に合わせて調律されており、しかもどれもが、最大の長所を、見合った短所と引き換えに手にしている。JSON がコメントを持たないことは、それをきれいな転送フォーマットにしているのと同じ性質だ。YAML があなたの意図を推測する癖は、悪名高い落とし穴を生んでいるのと同じ性質だ。TOML がすべてを明示させる頑固さは、データが深くなった途端に冗長になるのと同じ性質だ。どれもタダではないと見えた瞬間、「どれが一番いいか」は、実際に答えの出る二つの問いへとほどけていく——そして形式は、その答えの中から自ずと立ち上がってくる。

  1. これは機械どうしが交換するデータか、人が保守する設定か
  2. 人が保守するなら、形式にどれだけ推測させたいか

まず、同じものを三通りに書いてみる

問いに入る前に、同じ小さな設定を三形式で書き並べてみよう。同じデータ——サービス名、ポート、フラグ、リスト:

{
  "name": "billing-api",
  "port": 8080,
  "debug": false,
  "allowedHosts": ["localhost", "127.0.0.1"]
}
name = "billing-api"
port = 8080
debug = false
allowedHosts = ["localhost", "127.0.0.1"]
name: billing-api
port: 8080
debug: false
allowedHosts:
  - localhost
  - 127.0.0.1

並べると、違いは見た目だけに映る——波括弧か、key = value か、インデントか。だが見た目だけではない。どのレイアウトも、誰がこのファイルに打ち込み、誰が読み返すのかという、異なる賭けを刻んでいる。どちらに賭けるかを決めるには、あの二つの問いが要る。

問い一:機械のためのデータか、人のための設定か。コメントが手がかりだ

設定ファイルも、もちろん機械が読む——境界はそこではない。誰がそれを書き、保守するかだ。データファイルはプログラムが生み出し、プログラムが消費して、システム間で情報を運ぶ。設定ファイルは人が書き、人が世話をして、プログラムを動かす。ファイルがどちら側かを見分ける一番速い方法は、たった一つを問うことだ——コメントを書いてよいか

JSON には書けない。それは見落としではなく、意図的に外された。JSON を、構文もデータモデルも小さく広く相互運用できる純粋なデータ交換フォーマットにするため、そして読み手が命令と取り違えかねないものを一切含ませないためだ。二台のサーバーの間を飛び交うペイロードに、# TODO の出番はない。その厳格さこそが要点だ——コメント不可、末尾カンマ不可、キーはすべて引用符付き、書き方は一通り。この硬さは、向こう側の機械が毎回まったく同じ形で解析しなければならないときに、まさにあなたが望むものだ。そしてそれは、カンマ一つ、コメント一つで JSON が解析そのものを拒む理由でもある。

だから JSON は優れたデータ形式でありながら、扱いにくい設定形式なのだ。JSON ファイルを手で編集していて、「なぜ retries が 3 なのか」を説明しようとコメントに手を伸ばした瞬間、あなたは気づく——このファイルは転送中のデータではなく、データ形式の衣をまとった設定なのだと。エコシステムはとうにそれを黙認している。tsconfig.json は厳密な JSON ではなく JSONC(コメント付き JSON)だし、多くのツールが JSON5 を受け入れる。それらの変種が存在する唯一の理由は、JSON が意図的に取り除いたものを足し戻すことだ——それらのファイルは、初めから設定だったのだから。

こうして問い一はきれいに振り分けられる。

  • 機械どうしが交換するデータ——API レスポンス、キュー上のメッセージ、キャッシュの一項目、保存したり送ったりするデータ → JSON。ここでは、コメントがないことも、その融通のきかなさも、長所だ。YAML や TOML に手を伸ばせば、望んでいた厳格さを、どの機械も求めていない柔軟さと引き換えにすることになる。
  • 人が保守する設定——サービス設定、CI パイプライン、ツールの設定 → それは設定ファイルだ。問い二へ進もう。

正直な補足を一つ。設定を JSON で書くことは実際に多い——package.json.eslintrc.json——そしてそれは、ファイルが主に機械に管理され、人がめったに触らないなら問題ない。摩擦の大きさは、人が手で編集する頻度に比例する。package.json はたいていパッケージマネージャが書く。だが tsconfig.json はあなたが書く——だからこそツールチェーンは、そこにコメントを付け足さざるを得なかった。

問い二:形式にどれだけ推測させるか。YAML 対 TOML

設定ファイルだと決まれば、本当の分かれ道は YAML と TOML の間にあり、それは一つの性質に帰着する——形式がどれだけ推測するかだ。

YAML は最も推測する。port: 8080 と書けば数値と判断し、debug: false なら真偽値、host: localhost なら文字列。引用符も角括弧も、めったに打たない。その推論こそ YAML の書き味を軽くしている——そして次節で見るように、それが YAML の最も悪名高い落とし穴の源でもある。

TOML は設定形式の中で最も推測しない。しかも意図してそうしている。方針は「明示的でありながら、使いやすい」ことだ。文字列は JSON と同じく引用符で囲み、数値・真偽値・日付にはあいまいさのない書式があり、そして——ここが肝心だが——JSON が拒んだものを保っている。すなわちコメントと、人にやさしい [section] の区切りだ。分類できない裸の語は、こっそり変換されず、解析エラーになる。

だから設定形式の内側では、分かれ目は形と深さにある。

  • ほぼ平坦な設定——設定項目の並び、[database][server][logging] のようないくつかの区画 → TOML。大人になった INI ファイルのように読める——一目瞭然で、grep が効き、読み違えにくい。Rust の Cargo.toml、Python の pyproject.toml、そして無数の CLI がこれに落ち着いた理由だ。
  • 深くネストする設定——マップの木、オブジェクトのリスト、使い回したい値(Kubernetes マニフェスト、Ansible playbook、GitHub Actions ワークフロー、Docker Compose)→ YAML。深い木を広げるには、インデントのほうが TOML の繰り返される見出しよりずっと読みやすく、しかも YAML には再利用のための本物の仕掛け——アンカーとエイリアス——がある。

YAML に悪名をもたらした落とし穴:型の強制変換

YAML の「推測」が便利さから事故へ変わるのが、ここだ。最も有名なのが、いわゆるノルウェー問題である。

countries:
  - NO   # ノルウェー……それとも真偽値の false?

YAML 1.1 では——広く使われるパーサーの多く、たとえば PyYAML が今も既定でこれに従う——NO は文字列 "NO" ではない。真偽値の false だ。同じ強制変換は yesnoonoffyn も捕まえ、いずれも文字列ではなく真偽値として読まれる。国コードを並べた設定が、ノルウェーを音もなく取り落とし、プログラムに false を手渡す。(YAML 1.2 の core schema はこの規則を廃し、そこでは NO は文字列のままだ。だが既定が 1.1 の挙動のままの実行環境は少なくないので、安全だと決めてかからないほうがいい。)

真偽値だけの話でもなければ、1.1 だけの話でもない。トークン自体が数値に見えるせいで、YAML 1.2 の core schema でも発火する罠がある。

version: 1.10    # 1.1 でも 1.2 core でも:数値 1.1 になる——末尾のゼロが消える
build:   1e5     # 1.1 でも 1.2 core でも:100000 になる——科学表記と読まれる
time:    22:22   # YAML 1.1 のみ:1342 になる(60 進法:22×60 + 22)。1.2 core では文字列

これらは普通、エラーにはならない。どの値がどの型に変わるかは、使うパーサーとそれが適用する schema 次第だ——22:22 の 60 進法は YAML 1.1 のみ、一方 1.101e5 は 1.2 の core schema でも数値になる。暗黙の変換が起きると、誤った型が音もなく紛れ込み、それを知るのは本番でだ——あるバージョン照合が、数値 1.1 を文字列 "1.10" と突き合わせ、両者は違うと判定したときに。

対策は、一つの習慣を身につけることだ——人はテキストと読むが、パーサーは別物と読みかねない値には、すべて引用符を付ける。国コード、バージョン番号、git の SHA、先頭ゼロ付きのポート、時刻。version: "1.10"- "NO"。YAML の「推測」を、頼るものではなく警戒するものとして扱えば、YAML は安全だ。ただ、警戒すべきだと知っておく必要がある。(YAML の壊れ方の総覧——大声の解析エラーと、こうした静かな型変換——は別記事にまとめた:YAML はなぜ壊れるのか。)

そして、TOML がより安全に感じられる理由を説くのが、この対照だ。

country = NO       # エラー:TOML は推測しない——"NO" と書け、さもなくば解析を拒む
port    = 0700     # エラー:先頭ゼロは許されない

TOML は、YAML の音もない誤答を、大きな解析エラーに変える。分類できない裸の語は false にならず、設定を読み込む時点で明確な解析エラーになる。魅力を一言で言えばこうだ——TOML ではノルウェー問題を「うっかり」本番に出せない。あいまいな書き方が、そもそも不正だからだ。

とはいえ TOML は魔法ではない。ここに正直な境界がある。数値とそっくりの形をしたものは、やはり数値として解析される。

version = 1.10     # やはり浮動小数点数 1.1 になる——文字列が欲しければ "1.10" と書く

違いは免疫ではなく、予測可能性だ。TOML の規則は短い——数値とまったく同じ形なら数値である、それ以外はすべて引用符が要る、さもなくばエラー——一方 YAML は、自分の字面とは別物を指す裸の語(NOon22:22)を長々と抱えている。頭の中に収めておける短い規則は、ときどき驚かせてくる長い規則に勝る。

TOML の長所の請求書:深さ

TOML の明示性にも、それ自体の代償がある。そしてその請求は、データが深くなったときに来る。同じネストしたオブジェクトの並びを、両形式で見比べよう。

[[server]]
name = "alpha"
[server.limits]
maxConns = 100

[[server]]
name = "beta"
[server.limits]
maxConns = 200
server:
  - name: alpha
    limits:
      maxConns: 100
  - name: beta
    limits:
      maxConns: 200

TOML の [[server]][server.limits] という見出しはあいまいさこそないが、繰り返され、読み手は頭の中で木を組み立て直さねばならない。YAML では、インデントそのものが木だ。重くネストしたデータで、人が TOML を越えて JSON や YAML に手を伸ばすのはこれが理由だ——平坦から浅い層まで、TOML は快適そのもの。数階層も深くなると扱いにくくなり、YAML(防御的に引用符を付けたもの)のほうが読みやすくなる。

そのすべての底にある一つの原理

一歩下がると、三者は一本の軸の上に並ぶ——どれだけを推論に委ねるか——そして良し悪しを問わずどの性質も、形式がその軸のどこに位置するかから導かれる。

どれだけ推測するか得られるもの支払うもの
JSON何も推測しない。すべて明示、コメント不可厳格で、広く相互運用できる転送フォーマット手編集がつらい——コメント不可、末尾カンマ不可
TOMLほぼ推測しない。あいまいさはエラー安全で、明快で、grep の効く設定データが深くネストすると冗長
YAML最も推測する。裸の語から型を推論最も書き味が軽く、深い木にも広がるノルウェー問題と、音もない型の強制変換

代償のない行は一つもない。それが、あの議論の見落としていた点だ——探すべきは短所のない形式ではなく、このファイルについて、どの短所となら付き合えるかを選ぶことなのだ。手編集が面倒な転送フォーマットでも構わない——どうせめったに手編集しない。ネストで冗長になる設定形式でも構わない——設定が平坦でありさえすれば。型を推測する設定形式でも構わない——防御的に引用符を付け、レビュアーもどこを見るべきか知ってさえいれば。

決定表

ファイルは…使うもの理由
機械どうしが交換するデータ(API、キュー、ストレージ)JSON厳格さと広い相互運用性こそが目的だ。コメントは要らない
人の設定、ほぼ平坦な区画TOML明示的であいまいさがない——ノルウェー問題を踏めない
人の設定、深くネストするか再利用が多いYAMLインデントが深い木に広がり、アンカーで再利用——防御的に引用符を
手編集したいのに JSON に閉じ込められた設定JSONC / JSON5設定だと認めたのだから、コメントを足し戻す

ここのコンバーターは、テキストを往復させるだけでなく、まさにこうした罠を表に出すために作られている。YAML を YAML コンバーターに貼れば、チェック欄が YAML 1.1 の真偽値トラップ(あの引用符なしの no)と、いまにも浮動小数点数へ崩れそうなバージョン番号を指摘する。TOML を TOML コンバーターに貼れば、ISO 文字列へ格下げされた日時と、2^53 を超え、精度を失わないよう文字列として保持される整数を指摘する——ちょうど、離れる形式と入る形式とで言い分が食い違う地点だ。どちらも既定では YAML 1.2 core / 標準 TOML として解析する(文書が %YAML 1.1 で版を明示すれば、YAML の挙動はそれに従う)。リスクを指摘するだけで止めはしないので、別の実行環境がどう扱うかは、そのパーサーと schema 次第のままだ——それでも、差分を先に目で見ておくほうが、本番で出くわすよりましだ。JSON の塊を整えたり検証したりしたいだけなら、JSON フォーマッターでも片づく。

締めのチェックリスト

次にスレッドが「どの形式か」で詰まったら、どれが一番いいかを争うのはやめよう。順に問え。

  1. データか設定か。機械どうしが交換するデータ → JSON、そこで止まる。その厳格さは長所であって、直すべき欠点ではない。
  2. (人なら)平坦か深いか。ほぼ平坦な区画 → TOML。深くネストか再利用が多い → YAML。
  3. あいまいなスカラーに引用符を付けたか。とりわけ YAML で——国コード、バージョン、SHA、時刻、先頭ゼロの数字。付けなければ、パーサーが代わりに推測する。
  4. 形式と張り合っていないか。JSON にコメントを書きたくなる、あるいは TOML の [[section]] 見出しに溺れる——それは、ファイルが自分の形式を追い越した合図だ。耐える合図ではなく、乗り換える合図だ。

この四つの底にあるのは、覚えておく価値のある一行だ——最良の設定形式などない。あるのは、このファイルについて、その代償を自分が払える形式、ただそれだけだ。 まず「データか設定か」に答え、次に「平坦か深いか」に答え、YAML が放っておけば推測する箇所に引用符を付ける。そうすれば選択は好みの問題ではなくなり、一文で説明できるものになる。