YAMLをJSONに変換
YAMLからJSONへの変換は、YAML 1.2コアスキーマに従って、YAMLドキュメントをブラウザ内でJSONに変換します。これは聞こえ以上に重要です。PyYAMLやRubyが今も使っている1.2の前身のバージョンでは、引用符なしのトークンNO、yes、on、offはブール値として解決されます。そのため、ISO国コードのリストはノルウェーを失います。このツールではこうしたトークンは文字列のまま残り、1.1では異なる解決になるものにはすべて警告が付きます。
YAMLをJSONに変換はどう動きますか?
YAMLは単純に見えて、実際はそうではありません。仕様書はJSON、XML、TOMLの3つを合わせたものより長く、驚きのほとんどは1つの設計判断に由来します。引用符なしのスカラーは、構文で宣言するのではなくパターンで解決される、という点です。1つのトークンが何を意味するかは、読み手がどの解決表を使っているかで決まります。
ノルウェー問題はバージョンの問題
YAML 1.1では、y、yes、on、n、no、offがブール値として解決されていました。そのためcountries: [NO, SE, DK]は[false, "SE", "DK"]として読み込まれ、norway: noと書かれた設定ファイルは誰も意図していない意味を持っていました。YAML 1.2はこれらをすべて削除しました。六十進法の数値もあわせて削除されています。1.1が12:30を整数750として読んでいたのはこのためです。
厄介なのは、実際に使われているソフトウェアが割れていることです。PyYAMLのデフォルトローダーとRubyのPsychは1.1であり、Goのyaml.v3や最近のJavaScriptパーサーは1.2です。この変換ツールは1.2を適用した上で、1.1の読み手なら解釈が分かれるトークンを、名前を挙げてすべて通知します。有用な出力はJSONだけではなく、このファイルが曖昧だという情報だからです。
アンカーは上限つきで展開する
&nameがアンカーを定義し、*nameがそれを参照します。JSONには対応する機能がないため、各参照はコピーとして展開されます。この展開こそが「billion laughs」攻撃です。直前のアンカーを2回参照するアンカーを重ねると、10個で1,024ノード、20個で100万、30個で10億を超えます。200バイトのファイルがヒープを食い尽くしかねません。展開は100,000ノードで打ち止めとなり、そのことも報告されます。分かりやすいエラーと固まったタブの分かれ目はここにあります。
ブロックスカラーは改行を保持する
|は改行をそのまま保ち、>は改行をスペースに折りたたみます。末尾の改行をどうするかはチョンピングインジケーターが決めます。|は末尾の改行をちょうど1つ残し、|-はすべて取り除き、|+はすべて残します。この最後の違いが効いてくるのが、最終行が改行で終わっていなければならないPEM鍵やシェルスクリプトです。ブロックスカラーの中の#はコメントではなく内容です。これを取り除くパーサーは、あらゆるスクリプトからshebangを食べ取ってしまいます。
重複キーは隠さずに報告する
YAMLの仕様では、重複キーはエラーです。ほとんどのパーサーは黙って最後のものを採用します。このツールも最後のものを採用しますが、それはJSON.parseと同じ挙動であり、その上でどのキーが何行目かを通知します。設定ファイルの中の重複は、ほぼ必ず失敗したマージの結果だからです。
YAML
regions: [NO, SE] version: "007" retry: 012
JSON
{
"regions": ["NO", "SE"],
"version": "007",
"retry": 12
}
// warning: 012 — leading zero, 1.1 reads it as octalYAMLをJSONに変換が対応するオプションとエッジケースは?
| パラメータ | 型 | デフォルト | 動作とエッジケース |
|---|---|---|---|
| true / false | 真偽値 | 解決 | この2表記だけ、大文字小文字は問いません。これがYAML 1.1との決定的な違いであり、ノルウェーが生き延びる理由です。 |
| yes / no / on / off / y / n | 文字列 | フラグ付き | 1.2では文字列、1.1では真偽値です。それぞれ名前付きで報告されます。ランタイムごとにファイルの意味が変わることを知るためです。 |
| ~ / null / (空) | null | 解決 | 3つともJSONのnullになります。キーの後の空の値はnullであって、空文字列ではありません。 |
| 012 | 整数 | 12、フラグ付き | 1.2は先頭のゼロを10進と読み、1.1は8進の10と読みます。書き手が意図したのはおそらく文字列です。引用符で囲めばゼロを残せます。 |
| 0o17 / 0x1f | 整数 | 15 / 31 | 明示的な8進と16進です。1.2では8進に0oプレフィックスが必須です。だから素の017は15にならないのです。 |
| .inf / .nan | 文字列 | テキストのまま | JSONにはどちらも存在しないため、出力するとJSON.parseが拒否する文書になります。トークンはそのまま残し、フラグを付けます。 |
| アンカーとエイリアス | 展開 | 100,000ノード上限 | JSONに参照はないので、各*aliasはコピーになります。この上限のおかげで、billion-laughs展開がメモリを使い果たしません。 |
| マージキー(<<) | 未対応 | 報告 | 中途半端に実装するより拒否します。マージのセマンティクスは上書きや順序と絡み合い、微妙に間違いやすく、間違いに気づきにくいためです。 |
| 複数ドキュメント | ---区切り | 配列 | 複数の文書はJSON配列になります。最初の1つだけを変換したいときはオプションをオフにしてください。Kubernetesマニフェストの束はたいていそれを望みます。 |
| 入力サイズ | バイト | 8 MB | 解析はメインスレッドで動きます。上限を超えると、タブを固める代わりにツールが処理を断ります。 |
よくある質問
`no`がfalseではなく文字列になるのはなぜですか?
このパーサーはYAML 1.2コアスキーマを適用しており、ブール値はtrueとfalseだけだからです。YAML 1.1(PyYAMLのデフォルトローダーとRubyのPsychが今も使っているバージョン)では、no、yes、on、off、y、nがブール値として解決されます。ISO国コードのリストが黙ってノルウェーを失うのはこのためです。ブール値が欲しいならtrueかfalseと書いてください。文字列が欲しいなら、すでに手に入っています。
使っているツールがどのYAMLバージョンに対応していると想定すべきですか?
想定せずに確認してください。この割れは現実に存在し、実際にはバージョンが明示されないことも多いからです。PyYAMLのsafe_loadとRubyのPsychは1.1、Goのgopkg.in/yaml.v3と現行のJavaScriptパーサーの多くは1.2です。確実なのは、曖昧なものをすべて引用符で囲むことです。引用符付きのスカラーは、これまでに発表されたどのYAMLバージョンでも文字列なので、問題そのものが消えます。
アンカーはどうなったのですか?
展開されました。JSONには参照を表現する手段がないため、各*aliasはアンカーが持っていた内容の完全なコピーになります。これはデータとしては可逆で、構造としては非可逆です。ドキュメントの2箇所が同じオブジェクトだったという事実は失われます。それを保持する必要があるなら、JSONは変換先として適切ではありません。
エイリアスの展開を拒否されたのはなぜですか?
ネストしたエイリアスは乗算的に増えます。各アンカーが直前のものを2回参照する場合、1画面で読める大きさのファイルから30段で10億を超えるノードが生まれます。これがbillion-laughs攻撃で、実際にサービスを落とした例もあります。展開は100,000ノードで停止し、その旨を報告します。タブが死ぬまで増え続けたりはしません。
出力でも数値が引用符付きのままなのはなぜですか?
YAMLで引用符を付けたからです。引用符付きのスカラーは、どのバージョンのYAMLでも文字列になります。これは正しい挙動であり、通常は書き手の意図どおりです。version: "007"とversion: 007は実際に異なる値であり、書かれたままの形でラウンドトリップを生き残るのは片方だけです。
YAMLはアップロードされますか?
いいえ。パースも変換も、すでに開いているタブの中で実行されます。貼り付けながらNetworkパネルを見てみてください。YAMLはとりわけこの点が重要です。変換の必要に迫られるYAMLは、たいていKubernetesのマニフェスト、CIのワークフロー、docker-composeファイルであり、つまりインフラの詳細が詰まり、時には機密情報まで含まれたファイルです。