YAML データフォーマット

YAMLの落とし穴まとめ — ノルウェー問題から真偽値の罠まで

2026-03-11
目次

YAMLは設定ファイルやCI/CD、Kubernetesのマニフェストなど、あらゆる場面で使われるフォーマットです。「人間に読みやすい」が売りですが、実は「暗黙の型変換」という厄介な仕様が潜んでいて、知らないとハマります。

この記事では、YAMLでやらかしがちな落とし穴をまとめて紹介します。

ノルウェー問題(The Norway Problem)

YAMLの落とし穴で一番有名なやつです。国コードの一覧をYAMLで書いたとき、何が起こるか見てみましょう。

countries.yaml
countries:
- JP # 日本
- US # アメリカ
- NO # ノルウェー ← これが問題
- FR # フランス

YAML 1.1パーサーでこれを読み込むと、NO は文字列ではなく false になります。英語の「いいえ」として認識されて、booleanに変換されてしまうんですね。

parse-result.js
// YAML 1.1 パーサーでの結果
{
countries: ["JP", "US", false, "FR"]
}

YAML 1.2で修正されて true / false だけが真偽値になりましたが、PyYAMLなど今でもYAML 1.1ベースのパーサーは多いので、油断できません。

真偽値の罠

ノルウェー問題の根っこにあるのが、YAML 1.1の真偽値の定義が広すぎる問題です。以下の値は全部booleanとして解釈されます。

booleans-yaml11.yaml
# YAML 1.1 で true になる値
a: true
b: True
c: TRUE
d: yes
e: Yes
f: YES
g: on
h: On
i: ON
# YAML 1.1 で false になる値
j: false
k: False
l: FALSE
m: no
n: No
o: NO
p: off
q: Off
r: OFF

これ全部、文字列じゃなくてbooleanです。CI/CDの設定で on / off をトグルにしたいときや、フラグとして yes / no を使う場面で引っかかりがちです。

github-actions.yaml
# GitHub Actions の例
on:
push:
branches: [main]
# この "on" はトリガー指定のキーだが、
# YAML 1.1 パーサーでは true として解釈される可能性がある

YAML 1.2では truefalse(小文字のみ)に限定されました。

YAML 1.1YAML 1.2
true / falsebooleanboolean
True / Falseboolean文字列
TRUE / FALSEboolean文字列
yes / noboolean文字列
on / offboolean文字列

数値の暗黙変換

YAMLは数値も「よかれと思って」変換してきます。これが地味に厄介。

numbers.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 # Infinity
not_a_number: .nan # NaN

0108 になる問題は、設定ファイルでパーミッションを書くときに地味にハマります。

permissions.yaml
# ファイルパーミッション
permissions:
file: 0644 # YAML 1.1 では 420(10進数)として解釈される
dir: 0755 # YAML 1.1 では 493(10進数)として解釈される

文字列に見える数値

バージョン番号や郵便番号など、「見た目は文字列なのに数値にされる」パターンもよくあります。

versions.yaml
# バージョン番号の罠
python: 3.10 # 3.1 になる(浮動小数点数として解釈)
node: 20.11 # 20.11(これはたまたま同じ値)
ruby: 3.0 # 3 になる(整数として解釈)
# 正しくはクォートで囲む
python: "3.10"
ruby: "3.0"

3.103.1 になるのは、浮動小数点数として解釈されて末尾の 0 が消えるから。GitHub Actionsのワークフローでバージョン指定するときに実際にハマる人が多いやつです。

ci.yaml
# GitHub Actions での Python バージョン指定
strategy:
matrix:
python-version:
- 3.8
- 3.9
- "3.10" # クォート必須! なければ 3.1 になる
- "3.11"

郵便番号でも同じことが起きます。

address.yaml
# 先頭のゼロが消える
zip_code: 0120 # 数値の 120 になる(先頭のゼロが消失)
zip_code: "0120" # 文字列の "0120" として保持される

インデントの罠

YAMLはインデントで構造を決めるので、ここにも罠があります。

タブ文字の禁止

YAMLではインデントにタブ文字が使えません。スペースだけです。

indentation.yaml
# NG: タブ文字でインデント
parent:
child: value # パースエラーになる
# OK: スペースでインデント
parent:
child: value

エディタの設定でYAMLファイルはタブをスペースに変換するようにしておきましょう。

スペース数の不一致

YAMLはインデントのスペース数が固定ではありませんが、同じレベルのノードは揃える必要があります。

indent-mismatch.yaml
# NG: 同一レベルでインデントが異なる
parent:
child1: value1
child2: value2 # スペース3つ(child1は2つ)→ パースエラー
# OK: 同一レベルで統一
parent:
child1: value1
child2: value2

マッピングのインデント

リストとキーバリューを混在させるときも、インデントの解釈に注意が必要です。

mapping-indent.yaml
# ハイフンの後のスペースもインデントの一部
items:
- name: Alice
age: 30
- name: Bob
age: 25
# ハイフンとキーを同じ行に書く場合、
# 2行目以降はハイフン + スペース分のインデントが必要

複数行文字列

YAMLには複数行文字列の書き方がいくつかあって、それぞれ挙動が違います。

リテラルブロック(|)と折り畳みブロック(>

multiline.yaml
# | : リテラルブロック(改行をそのまま保持)
literal: |
1行目
2行目
3行目
# 結果: "1行目\n2行目\n3行目\n"
# > : 折り畳みブロック(改行をスペースに変換)
folded: >
1行目
2行目
3行目
# 結果: "1行目 2行目 3行目\n"

末尾改行の制御

末尾の改行をどう扱うか、-(strip)と +(keep)で制御できます。

chomp.yaml
# デフォルト: 末尾に改行1つ
default: |
text
# |- : strip(末尾の改行を除去)
strip: |-
text
# |+ : keep(末尾の改行をすべて保持)
keep: |+
text
記法末尾改行結果
|1つ残す(デフォルト)"text\n"
|-すべて除去"text"
|+すべて保持"text\n\n"

この違いを知らないと、テンプレート生成時に「なぜか空行が入る(消える)」で悩むことになります。

対策

ここまでいろいろ紹介しましたが、対策はシンプルです。

クォートを付ける

一番確実な方法。文字列にしたい値にはクォートを付ける。これだけです。

quoted.yaml
country: "NO" # 文字列として確実に扱われる
version: "3.10" # バージョン番号も安全
flag: "yes" # 真偽値に変換されない
permissions: "0644" # 8進数に変換されない

ちなみにダブルクォート(")では \n が改行になりますが、シングルクォート(')ではそのまま \n という文字列になります。

yamllintを使う

yamllint でYAMLファイルの構文・スタイルをチェックできます。

Terminal window
# インストール
pip install yamllint
# チェック実行
yamllint config.yaml

truthy ルールを有効にすると、yes / no / on / off のような曖昧な真偽値を検出してくれるので安心です。

.yamllint.yml
rules:
truthy:
allowed-values: ["true", "false"]
check-keys: true

YAML 1.2対応パーサーを使う

YAML 1.2対応のパーサーを選ぶだけで、暗黙変換の問題をかなり回避できます。

言語YAML 1.1 パーサーYAML 1.2 パーサー
PythonPyYAMLruamel.yaml, strictyaml
JavaScriptjs-yaml(デフォルト)js-yaml(CORE_SCHEMA指定時)
Gogo-yaml v2go-yaml v3
RubyPsych(Ruby < 3.2)Psych(Ruby >= 3.2)

JSON変換で確認する

YAMLの解釈が不安なときは、JSONに変換してみるのが手っ取り早いです。JSONには暗黙の型変換がないので、パーサーがどう解釈しているか一目瞭然です。

Terminal window
# 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の落とし穴はほとんど「暗黙の型変換」が原因です。振り返ると——

  • ノルウェー問題: NOfalse に変換される。YAML 1.1の広すぎる真偽値定義が原因
  • 真偽値の罠: yes / no / on / off なども真偽値になる(YAML 1.1)
  • 数値の罠: 010 が8進数、3.103.1 になる
  • インデント: タブ禁止、スペース数の統一が必須
  • 複数行文字列: |> の違い、末尾改行の制御を理解する

対策はとにかくクォートを付ける。これが一番効きます。yamllintでの自動チェックやYAML 1.2対応パーサーも合わせて使えば、かなり安全にYAMLを扱えるようになります。

YAMLは便利ですが、柔軟さゆえの罠があります。知っておくだけで防げるものがほとんどなので、この記事が参考になれば幸いです。

ヒヨリヒヨリ

NO がノルウェーじゃなくて false になるなんてびっくりだよね!あたしもバージョン番号で 3.103.1 になって30分くらい悩んだことがあるんだ…。とりあえず迷ったらクォートで囲む!これだけで大体の罠は防げるよ。さあ、試してみよう!