YAMLは設定ファイルやCI/CD、Kubernetesのマニフェストなど、あらゆる場面で使われるフォーマットです。「人間に読みやすい」が売りですが、実は「暗黙の型変換」という厄介な仕様が潜んでいて、知らないとハマります。
この記事では、YAMLでやらかしがちな落とし穴をまとめて紹介します。
ノルウェー問題(The Norway Problem)
YAMLの落とし穴で一番有名なやつです。国コードの一覧をYAMLで書いたとき、何が起こるか見てみましょう。
countries: - JP # 日本 - US # アメリカ - NO # ノルウェー ← これが問題 - FR # フランスYAML 1.1パーサーでこれを読み込むと、NO は文字列ではなく false になります。英語の「いいえ」として認識されて、booleanに変換されてしまうんですね。
// YAML 1.1 パーサーでの結果{ countries: ["JP", "US", false, "FR"]}YAML 1.2で修正されて true / false だけが真偽値になりましたが、PyYAMLなど今でもYAML 1.1ベースのパーサーは多いので、油断できません。
真偽値の罠
ノルウェー問題の根っこにあるのが、YAML 1.1の真偽値の定義が広すぎる問題です。以下の値は全部booleanとして解釈されます。
# YAML 1.1 で true になる値a: trueb: Truec: TRUEd: yese: Yesf: YESg: onh: Oni: ON
# YAML 1.1 で false になる値j: falsek: Falsel: FALSEm: non: Noo: NOp: offq: Offr: OFFこれ全部、文字列じゃなくてbooleanです。CI/CDの設定で on / off をトグルにしたいときや、フラグとして yes / no を使う場面で引っかかりがちです。
# GitHub Actions の例on: push: branches: [main]# この "on" はトリガー指定のキーだが、# YAML 1.1 パーサーでは true として解釈される可能性があるYAML 1.2では true と false(小文字のみ)に限定されました。
| 値 | YAML 1.1 | YAML 1.2 |
|---|---|---|
true / false | boolean | boolean |
True / False | boolean | 文字列 |
TRUE / FALSE | boolean | 文字列 |
yes / no | boolean | 文字列 |
on / off | boolean | 文字列 |
数値の暗黙変換
YAMLは数値も「よかれと思って」変換してきます。これが地味に厄介。
# 8進数(YAML 1.1)octal_old: 010 # 8 として解釈される(0始まりは8進数)
# 8進数(YAML 1.2)octal_new: 0o10 # 8 として解釈される
# 16進数hex: 0x1A # 26 として解釈される
# アンダースコア区切りlarge: 1_000_000 # 1000000 として解釈される(YAML 1.1)
# 浮動小数点の特殊値infinity: .inf # Infinitynot_a_number: .nan # NaN010 が 8 になる問題は、設定ファイルでパーミッションを書くときに地味にハマります。
# ファイルパーミッションpermissions: file: 0644 # YAML 1.1 では 420(10進数)として解釈される dir: 0755 # YAML 1.1 では 493(10進数)として解釈される文字列に見える数値
バージョン番号や郵便番号など、「見た目は文字列なのに数値にされる」パターンもよくあります。
# バージョン番号の罠python: 3.10 # 3.1 になる(浮動小数点数として解釈)node: 20.11 # 20.11(これはたまたま同じ値)ruby: 3.0 # 3 になる(整数として解釈)
# 正しくはクォートで囲むpython: "3.10"ruby: "3.0"3.10 が 3.1 になるのは、浮動小数点数として解釈されて末尾の 0 が消えるから。GitHub Actionsのワークフローでバージョン指定するときに実際にハマる人が多いやつです。
# GitHub Actions での Python バージョン指定strategy: matrix: python-version: - 3.8 - 3.9 - "3.10" # クォート必須! なければ 3.1 になる - "3.11"郵便番号でも同じことが起きます。
# 先頭のゼロが消えるzip_code: 0120 # 数値の 120 になる(先頭のゼロが消失)zip_code: "0120" # 文字列の "0120" として保持されるインデントの罠
YAMLはインデントで構造を決めるので、ここにも罠があります。
タブ文字の禁止
YAMLではインデントにタブ文字が使えません。スペースだけです。
# NG: タブ文字でインデントparent: child: value # パースエラーになる
# OK: スペースでインデントparent: child: valueエディタの設定でYAMLファイルはタブをスペースに変換するようにしておきましょう。
スペース数の不一致
YAMLはインデントのスペース数が固定ではありませんが、同じレベルのノードは揃える必要があります。
# NG: 同一レベルでインデントが異なるparent: child1: value1 child2: value2 # スペース3つ(child1は2つ)→ パースエラー
# OK: 同一レベルで統一parent: child1: value1 child2: value2マッピングのインデント
リストとキーバリューを混在させるときも、インデントの解釈に注意が必要です。
# ハイフンの後のスペースもインデントの一部items: - name: Alice age: 30 - name: Bob age: 25
# ハイフンとキーを同じ行に書く場合、# 2行目以降はハイフン + スペース分のインデントが必要複数行文字列
YAMLには複数行文字列の書き方がいくつかあって、それぞれ挙動が違います。
リテラルブロック(|)と折り畳みブロック(>)
# | : リテラルブロック(改行をそのまま保持)literal: | 1行目 2行目 3行目# 結果: "1行目\n2行目\n3行目\n"
# > : 折り畳みブロック(改行をスペースに変換)folded: > 1行目 2行目 3行目# 結果: "1行目 2行目 3行目\n"末尾改行の制御
末尾の改行をどう扱うか、-(strip)と +(keep)で制御できます。
# デフォルト: 末尾に改行1つdefault: | text
# |- : strip(末尾の改行を除去)strip: |- text
# |+ : keep(末尾の改行をすべて保持)keep: |+ text| 記法 | 末尾改行 | 結果 |
|---|---|---|
| | 1つ残す(デフォルト) | "text\n" |
|- | すべて除去 | "text" |
|+ | すべて保持 | "text\n\n" |
この違いを知らないと、テンプレート生成時に「なぜか空行が入る(消える)」で悩むことになります。
対策
ここまでいろいろ紹介しましたが、対策はシンプルです。
クォートを付ける
一番確実な方法。文字列にしたい値にはクォートを付ける。これだけです。
country: "NO" # 文字列として確実に扱われるversion: "3.10" # バージョン番号も安全flag: "yes" # 真偽値に変換されないpermissions: "0644" # 8進数に変換されないちなみにダブルクォート(")では \n が改行になりますが、シングルクォート(')ではそのまま \n という文字列になります。
yamllintを使う
yamllint でYAMLファイルの構文・スタイルをチェックできます。
# インストールpip install yamllint
# チェック実行yamllint config.yamltruthy ルールを有効にすると、yes / no / on / off のような曖昧な真偽値を検出してくれるので安心です。
rules: truthy: allowed-values: ["true", "false"] check-keys: trueYAML 1.2対応パーサーを使う
YAML 1.2対応のパーサーを選ぶだけで、暗黙変換の問題をかなり回避できます。
| 言語 | YAML 1.1 パーサー | YAML 1.2 パーサー |
|---|---|---|
| Python | PyYAML | ruamel.yaml, strictyaml |
| JavaScript | js-yaml(デフォルト) | js-yaml(CORE_SCHEMA指定時) |
| Go | go-yaml v2 | go-yaml v3 |
| Ruby | Psych(Ruby < 3.2) | Psych(Ruby >= 3.2) |
JSON変換で確認する
YAMLの解釈が不安なときは、JSONに変換してみるのが手っ取り早いです。JSONには暗黙の型変換がないので、パーサーがどう解釈しているか一目瞭然です。
# Python で YAML → JSON 変換python -c "import yaml, json, sys; print(json.dumps(yaml.safe_load(open(sys.argv[1])), indent=2, ensure_ascii=False))" config.yaml当サイトのツールで試す
当サイトの YAML-JSON コンバーター にYAMLを入力すると、リアルタイムでJSONに変換されます。暗黙の型変換が起きていないか、実際に試して確認してみてください。
まとめ
YAMLの落とし穴はほとんど「暗黙の型変換」が原因です。振り返ると——
- ノルウェー問題:
NOがfalseに変換される。YAML 1.1の広すぎる真偽値定義が原因 - 真偽値の罠:
yes/no/on/offなども真偽値になる(YAML 1.1) - 数値の罠:
010が8進数、3.10が3.1になる - インデント: タブ禁止、スペース数の統一が必須
- 複数行文字列:
|と>の違い、末尾改行の制御を理解する
対策はとにかくクォートを付ける。これが一番効きます。yamllintでの自動チェックやYAML 1.2対応パーサーも合わせて使えば、かなり安全にYAMLを扱えるようになります。
YAMLは便利ですが、柔軟さゆえの罠があります。知っておくだけで防げるものがほとんどなので、この記事が参考になれば幸いです。
NO がノルウェーじゃなくて false になるなんてびっくりだよね!あたしもバージョン番号で 3.10 が 3.1 になって30分くらい悩んだことがあるんだ…。とりあえず迷ったらクォートで囲む!これだけで大体の罠は防げるよ。さあ、試してみよう!