本文へスキップ
jsonbeautifiers
日本語

JSONかYAMLかTOMLか、どれに手を伸ばすか

この3つの形式の違いは、表現できることよりも、壊れ方にあります。そして時間を取られるのは壊れ方のほうです。

このページの記述はすべて実測か出典付きです。そのどちらでもない場合は、そのことを明記しています。

デプロイのパイプラインがYAMLファイルから国の一覧を読んでいます。誰かがノルウェーをISOコードNOで追加すると、パイプラインはその市場をスキップし始め、どこにもエラーは出ません。アプリケーションに届いた値は真偽値のfalseでした。

どの形式を選ぶかを決めるべきなのは、こういう事柄です。「コメント対応: あり/なし」という表ではありません。3つともキーが文字列で値が何かのマップを保持できます。分けるのは、誰も見ていないときに何をしてくるかです。

JSON: 退屈であり、それこそが要点

JSONは通信用の形式です。型は6つ、トークン間に許される空白文字は4つ(RFC 8259によりスペース、タブ、復帰、改行)、コメントなし、末尾カンマなし、日付型なし、そして数値型はちょうど1つで、読む側はそれをfloat64と解釈して構いません。重要な箇所で仕様が不足している点も少しあり、とりわけキーの重複では、RFCがキーは一意であるべき(SHOULD)と述べたうえで挙動を未定義のままにしています。JavaScriptもPythonも最後のものを採ります。

その美点は完全に技術外のところにあります。どの言語も標準ライブラリにパーサーを同梱しています。どのHTTPクライアントも扱い方を知っています。バージョンのずれは事実上ありません。2008年に書かれたJSON文書は今日もどこでも同じようにパースされます。ネットワークの1ホップ、ログの1行、メッセージキュー、キャッシュのために直列化するとき、他の2形式が持つ人間向けの機能はあなたに何ももたらさず、普遍性は多くをもたらします。

失敗の形はよく踏み固められていて、たいていは数値の話です。Number.MAX_SAFE_INTEGERは9007199254740991で、それを超えるIDは黙って書き換えられます。これはそれ自体で1本の記事です。日付は慣習として文字列で、その慣習を強制するものはありません。これもそれ自体で1本の記事です。どちらも、転送のために別の形式を選ぶ理由にはなりません。用心する理由です。

YAML: 本物の使い心地、本物の請求書

人がYAMLを選ぶのは、それが優雅だからではありません。Kubernetesのマニフェストや CIのパイプラインは人が毎日手で編集するものであり、JSONは手で編集するのが本当に不快だからです。コメントは書けず、引用符は必須で、抜けたカンマは400行上にあります。YAMLはコメントを与え、読める複数行文字列を与え、句読点の雑音をなくします。それには価値があります。

その代金がこちらです。

ノルウェー問題

YAML 1.1は引用符のないnoyesonoffynを真偽値として解決します。YAML 1.2のcoreスキーマはそうせず、文字列のままにします。同じ文書、同じキー、答えは2つ。

a: no

YAML 1.2のcore解決ではこの値は文字列"no"です。1.1の規則では真偽値のfalseです。どちらになるかはファイルではなくライブラリ次第で、PyYAMLとRubyのPsychは1.1の規則で解決し、js-yamlは1.2に従います。Goのyaml.v3はその中間で、noは文字列として解決しますが、対象のフィールドが型付きのboolであれば1.1の綴りも受け付けます。同じ設定ファイルを読むPythonのサービスとNodeのサービスが値について食い違い、しかもどちらも何もログに残しません。

対策は、別の何かと取り違えられうる文字列をすべて引用符で囲むことです。国コード、バージョン番号(1.10は浮動小数点数、"1.10"は違う)、ゼロで始まるもの、そして利用者が入力する値すべて。プログラムでYAMLを生成するなら、自分のレビューを当てにせず、出力側に守りの姿勢で引用符を付けさせてください。

空白は構文であり、タブは違法

インデントが構造を担うので、ずれた行はエラーではなく別の文書になります。さらに悪いことに、YAML仕様はインデントにタブ文字を使うことを明確に禁じています。タブを挿入する設定のエディタは、端末では目に見えない文字についてのメッセージとともにパースに失敗するファイルを生み出します。エディタをファイル種別ごとに設定して、この件は考えないで済むようにしましょう。

アンカーは出口で展開される

アンカーとエイリアスは、ブロックを一度定義して再利用させてくれます。

defaults: &defaults
  timeout: 30
  retries: 3

staging:
  <<: *defaults
  host: stage.internal

ほぼ同一のサービス定義を40個保守している人にYAMLを売り込むのがこの機能です。同時に、データモデルのほうには存在しない機能でもあります。このファイルをJSONに変換すると、マージキーは解決され、エイリアスは展開され、defaultsが丸ごとstagingの中に現れます。YAMLへ戻せば、リテラルな複製が2つできます。厳密に言えば何も間違っていませんが、あなたが保守していたものは消えています。アンカーに寄りかかったYAMLファイルは、本当は変換可能ではなく、一度読めるだけのものです。

yaml.loadは設定を実行する

完全なYAMLは、任意のオブジェクトを構築する言語固有のタグに対応しています。Pythonでは、!!python/object/apply:os.systemを含む文書がパース中にコマンドを実行しうるということです。yaml.safe_loadは標準的な型しか構築しない版で、自分で書いたのではないものにはこちらを使うべきです。PyYAMLは最終的に明示的なloader引数を要求することで間違いにくくしましたが、それ以前のコードは山ほどあり、ほかの多くの言語では安全でない既定が関数呼び出し1つ分の距離に残っています。

import yaml

with open("config.yaml") as f:
    cfg = yaml.safe_load(f)   # yaml.load ではない

スーパーセットという細部

YAML 1.2はJSONのスーパーセットとして設計され、仕様は妥当なJSON文書がすべて妥当なYAML 1.2文書でもあると述べています。ですから1.2のパーサーはあなたのJSONを読みます。YAML 1.1はそうとも言い切れません。コロンのあとに空白を求めるので、詰めて書いた{"a":1}はそこではパースエラーですし、1.1の解決規則は依然としてあなたの文字列の一部を真偽値に変えます。「JSONをYAMLパーサーに渡せばよい」に寄りかかるなら、まず自分のライブラリがどの版を実装しているか確かめてください。いずれにせよ、逆方向へはきれいに進めます。YAML→JSON変換ツールをどうぞ。

TOML: 入れ子にするまでは曖昧さがない

TOMLが存在するのは、INIファイルが快適で不正確だったからです。その不正確さを直しました。整数と浮動小数点数は別の型、真偽値はtruefalseだけ、そして日付と時刻の型が本物として4つ(オフセット付き日時、ローカル日時、ローカル日付、ローカル時刻)文法に組み込まれており、文字列に紛れ込ませる必要がありません。コメントは一級市民です。同じキーを二度定義するのは未定義動作ではなく厳格なエラーで、これは小さなことながらマージ事故の一類型を実際に捕まえます。

平坦、あるいは浅い設定であれば3つの中で最良です。Cargo.tomlpyproject.tomlが分かりやすい例で、セクションがいくつか、値は文字列とリスト、入れ子はときどき1段。曖昧なものはなく、安全のために引用符を付ける必要もありません。

データが木構造になると急に見苦しくなります。深い入れ子は、長いドット付きの見出しか、長いドット付きのキーかを強います。

[servers.production.database.replica]
host = "10.0.0.4"
port = 5432

そしてオブジェクトの配列には、要素ごとに繰り返す二重角括弧のテーブル配列形式が必要です。

[[targets]]
name = "web"
port = 8080

[[targets]]
name = "worker"
port = 8081

2件なら読めます。3フィールドの項目が30件、しかもインラインテーブルは1行に収めなければならない、となれば、あなたは形式と戦っています。設定が本当に階層的なら、TOMLは形が合っておらず、階層を1つ増やすたびにそれを感じることになります。

どれもくれないもの

10進の型。 3つとも与えるのは浮動小数点数、つまり2進の近似です。金額は依然として、最小単位の整数か文字列に属します。

バイナリデータ。 JSONとTOMLには表現が一切ないので、文字列の中のbase64になります。YAMLには!!binaryタグがあり、それは機能しますが、他の2つへの変換を生き延びません。

形式に付属するスキーマ。 JSON Schemaが成熟した選択肢であり、YAML 1.2は同じデータモデルに対応づくので、それでYAMLも検証できます。実務のYAML検証はほとんどそうやって行われています。TOMLには同程度に普及した相当物がありません。

変換をくぐり抜けるコメント。 これが一方通行の扉です。コメントは構文に住み、データモデルには住んでいません。ですからJSONに変換されたYAMLやTOMLのファイルはコメントを永久に失い、それを取り戻す気の利いたツールは存在しません。あるファイルのコメントが荷重を担っているなら、真実の源はそのファイルであり、JSONはあなたが生成する成果物にすぎません。JSONにコメントがないことは意図的であり、この非対称性の理由でもあります。

選択を、問いの形で

読むのは機械だけか。 JSONです。APIにYAMLを話させてはいけません。

人が毎週編集し、しかも階層的か。 YAMLです。引用符の規律とsafe_loadを添えて。

人が編集し、内容はおおむね平坦なセクションのスカラーか。 TOMLです。失うものはなく、曖昧さのない型が手に入ります。

コメントを生き残らせる必要があるか。 どれを選ぶにせよ、そのファイルが真実の源です。下流へ生成し、生成された複製を編集しないこと。

値を作るのは開発者以外の人やUIか。 JSONを、プログラムで生成し、スキーマで検証してください。上に挙げたYAMLの罠はどれも、誰かが打ち込んだ文字列が引き金です。

いままさに相互変換しているところか。 JSON→YAML変換ツールで行い、出力を信用せずに読んでください。とくに真偽値を。そしてデプロイに関わる何かに届く前に、結果をバリデーターに通してください。