YAMLのNOやonがfalseになる js-yaml 4とPyYAMLの差を実測

設定ファイルに country: NO と書いたらノルウェーが false になった、という話は「Norway problem」として知られている。ただ、実際にそうなるかはパーサー次第で、JavaScript でよく使われる js-yaml 4 では "NO" のまま読まれる。同じファイルを Python と Node.js の両方で読むと、値が食い違う。

この記事では、同じ YAML を PyYAML 6.0.3 と js-yaml 4.1.0 で読み、どの値がどう変わるかを実測する。差の出どころは YAML 1.1 と 1.2 の型解決の違いにある。

同じファイルを2つのパーサーで読む

次の sample.yaml を用意した。どれも設定ファイルで普通に書きそうな値である。

1
2
3
4
5
6
7
8
9
10
11
country: NO
answer: no
enabled: on
flag: y
version: 1.10
mode: 0755
octal12: 0o755
time: 12:30
date: 2026-10-28
nulls: ~
hex: 0x1F

実行環境は Windows 11、Node.js v24.11.1 + js-yaml 4.1.0、Python 3.11.9 + PyYAML 6.0.3。結果を並べる。

キー 原文 js-yaml 4.1.0 PyYAML 6.0.3
country NO "NO"(文字列) False
answer no "no"(文字列) False
enabled on "on"(文字列) True
flag y "y"(文字列) 'y'(文字列)
version 1.10 1.1(数値) 1.1(float)
mode 0755 755(10進数) 493(8進数として解釈)
octal12 0o755 493 '0o755'(文字列)
time 12:30 "12:30"(文字列) 750(60進数)
date 2026-10-28 Date(UTC 0時) datetime.date
nulls ~ null None
hex 0x1F 31 31

食い違いは NO / no / on、0755 と 0o755、12:30 の 6 行。version: 1.10 はどちらでも 1.1 になり、末尾の 0 が消える。y はどちらも文字列だった。

なぜ食い違うのか: YAML 1.1 と 1.2 の型解決

引用符のない値(plain scalar)は、パーサーが正規表現で型を決める。この表が YAML 1.1 と 1.2 で違う。

YAML 1.1 の型定義(yaml.org の Boolean Language-Independent Type)は、真偽値として y|Y|yes|Yes|YES|n|N|no|No|NO、true|True|TRUE|false|False|FALSE、on|On|ON|off|Off|OFF を挙げている。同じく整数型の定義には、8 進数 [-+]?0[0-7_]+ と 60 進数 [-+]?[1-9][0-9_]*(:[0-5]?[0-9])+ がある。0755 が 493、12:30 が 12×60+30 = 750 になるのはこのためだ。

YAML 1.2.2 仕様の Core Schema(10.3.2 Tag Resolution)では、真偽値は true | True | TRUE | false | False | FALSE だけになり、8 進数は 0o [0-7]+、10 進数は [-+]? [0-9]+ と定められている。60 進数の規則は無い。仕様の 1.2 節も、1.2 では問題の多かった暗黙の型付けの推奨を取り除いたと書いている。

PyYAML は PyPI の説明で「YAML 1.1 parser」を名乗っており、上の表の PyYAML 列は 1.1 の規則どおりになっている。ただし y は 1.1 の真偽値の表にあるのに文字列のままだった。PyYAML 6.0.3 の実装が y / n を真偽値として扱っていないためで、「1.1 準拠のパーサーなら全部同じ」とも言えない。

js-yaml は README で 1.2 対応をうたい、CHANGELOG の 4.0.0(2021-01-03)に「数値を YAML 1.1 ではなく 1.2 に従って解析する」「01234 は 10 進数、0o1234 は 8 進数、1:23 は 60 進数ではなく文字列」と書かれている。yes / no / on / off を真偽値にしない変更は、さらに前の 1.0.0 の項に記載がある。

js-yaml の dump は 1.1 の読み手に配慮している

逆向き、つまり JavaScript のオブジェクトを YAML に書き出すときも差が出る。次の dump.js で確かめた。

1
2
3
4
5
6
7
const yaml = require('js-yaml');
const obj = { country: 'NO', answer: 'no', enabled: 'on', flag: 'y',
version: '1.10', mode: '0755', time: '12:30' };
console.log('--- default');
process.stdout.write(yaml.dump(obj));
console.log('--- noCompatMode: true');
process.stdout.write(yaml.dump(obj, { noCompatMode: true }));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--- default
country: 'NO'
answer: 'no'
enabled: 'on'
flag: 'y'
version: '1.10'
mode: '0755'
time: '12:30'
--- noCompatMode: true
country: NO
answer: no
enabled: on
flag: y
version: '1.10'
mode: '0755'
time: 12:30

既定では 'NO' や '12:30' に引用符が付く。js-yaml 4 自身はこれらを引用符なしでも文字列と読むが、出力を YAML 1.1 のパーサー(PyYAML など)が読んだときに型が変わらないよう、わざと付けている。README は noCompatMode を「古い YAML バージョンとの互換を試みない」オプションと説明しており、true にすると引用符が外れる。js-yaml で書いた YAML を Python 側でも読むなら、noCompatMode は付けないほうがよい。

実際に試す

1. 両方のパーサーで同じファイルを読む

前提: Node.js(v24.11.1 で確認)と Python 3(3.11.9 で確認)。作業用ディレクトリで次を実行する。

1
2
3
npm init -y
npm install js-yaml@4.1.0
pip install PyYAML==6.0.3

上の sample.yaml を保存し、Node.js 用の load.js を作る。

1
2
3
4
5
6
7
8
// load.js : 値と型を1行ずつ表示する
const fs = require('fs');
const yaml = require('js-yaml');
const doc = yaml.load(fs.readFileSync(process.argv[2] || 'sample.yaml', 'utf8'));
for (const [k, v] of Object.entries(doc)) {
const t = v instanceof Date ? 'Date' : v === null ? 'null' : typeof v;
console.log(`${k.padEnd(8)} ${t.padEnd(7)} ${v instanceof Date ? v.toISOString() : JSON.stringify(v)}`);
}

Python 用の load.py も作る。

1
2
3
4
5
6
# load.py : 値と型を1行ずつ表示する
import sys, yaml
with open(sys.argv[1] if len(sys.argv) > 1 else 'sample.yaml', encoding='utf-8') as f:
doc = yaml.safe_load(f)
for k, v in doc.items():
print(f"{k:<8} {type(v).__name__:<7} {v!r}")
1
2
node load.js sample.yaml
python load.py sample.yaml
1
2
3
4
5
6
7
8
9
10
11
country  string  "NO"
answer string "no"
enabled string "on"
flag string "y"
version number 1.1
mode number 755
octal12 number 493
time string "12:30"
date Date 2026-10-28T00:00:00.000Z
nulls null null
hex number 31
1
2
3
4
5
6
7
8
9
10
11
country  bool    False
answer bool False
enabled bool True
flag str 'y'
version float 1.1
mode int 493
octal12 str '0o755'
time int 750
date date datetime.date(2026, 10, 28)
nulls NoneType None
hex int 31

2. 文字列のつもりの値は引用符で囲む

どちらのパーサーでも同じ結果にする一番確実な方法は、文字列として扱いたい値を引用符で囲むことだ。fixed.yaml を作る。

1
2
3
4
5
6
7
country: "NO"
answer: "no"
enabled: "on"
version: "1.10"
mode: "0755"
time: "12:30"
date: "2026-10-28"
1
2
node load.js fixed.yaml
python load.py fixed.yaml
1
2
3
4
5
6
7
country  string  "NO"
answer string "no"
enabled string "on"
version string "1.10"
mode string "0755"
time string "12:30"
date string "2026-10-28"
1
2
3
4
5
6
7
country  str     'NO'
answer str 'no'
enabled str 'on'
version str '1.10'
mode str '0755'
time str '12:30'
date str '2026-10-28'

両方とも全て文字列になり、1.10 の末尾の 0 も残った。ファイルのパーミッションのように 8 進数の数値として渡したい値は、受け取る側がどちらの規則で読むかを確かめてから書く。js-yaml 4 で 493 を得たいなら 0o755、PyYAML なら 0755 で、同じ綴りでは両立しない。数値ではなく文字列 "0755" で受け取り、アプリ側で parseInt(s, 8) や int(s, 8) で変換するほうが食い違いが起きない。

3. 日付を Date にしたくないときは CORE_SCHEMA

js-yaml 4.1.0 の既定のスキーマ(DEFAULT_SCHEMA)は 2026-10-28 を Date にする。日付を文字列のまま受け取りたいなら、load に schema: yaml.CORE_SCHEMA を渡す。

1
2
3
4
const yaml = require('js-yaml');
const src = 'date: 2026-10-28\nmode: 0755\ntime: 12:30\n';
console.log(JSON.stringify(yaml.load(src)));
console.log(JSON.stringify(yaml.load(src, { schema: yaml.CORE_SCHEMA })));
1
2
{"date":"2026-10-28T00:00:00.000Z","mode":755,"time":"12:30"}
{"date":"2026-10-28","mode":755,"time":"12:30"}

CORE_SCHEMA では date が原文どおりの文字列になる。mode と time は既定と同じだった。

ハシトシステムの YAML⇄JSON 変換では

ブラウザで使える YAML⇄JSON 変換(ハシトシステム) は、2026-10-28 時点で js-yaml 4.1.0 を読み込み、YAML→JSON は jsyaml.load(text) を既定のスキーマのまま呼んで JSON.stringify している。JSON→YAML は jsyaml.dump(obj, {indent, lineWidth: -1, noRefs: true}) で、noCompatMode は指定していない。

ツールと同じ呼び方で上の sample.yaml を変換すると、JSON 側は次のようになる(抜粋)。

1
2
3
4
5
"country": "NO",
"version": 1.1,
"mode": 755,
"time": "12:30",
"date": "2026-10-28T00:00:00.000Z",

つまり、このツールの結果は YAML 1.2 の読み方で、NO や on は文字列のまま出る。Ansible や PyYAML を使うツールなど YAML 1.1 系のパーサーが読む設定ファイルを確認したいときは、「ツールで文字列に見えたから大丈夫」とは言えない点に注意したい。日付は ISO 8601 の UTC 時刻つき文字列に、0755 は 755 に変わる。逆に JSON→YAML の出力は 'NO' や '12:30' に引用符が付くので、1.1 系のパーサーに渡しても型は変わらない。

JSON 側で値が変わる別のパターン(2^53 を超える整数や 1.0)は、JSON 整形で値が変わる記事 で扱った。

まとめ

  • PyYAML 6.0.3(YAML 1.1)は NO / no / on を真偽値、0755 を 8 進数 493、12:30 を 750 と読む。js-yaml 4.1.0(YAML 1.2)はそれぞれ文字列・10 進数 755・文字列のまま。
  • 1.10 はどちらでも 1.1 になる。y は PyYAML でも文字列だった。
  • js-yaml の dump は既定で 1.1 の読み手に配慮して 'NO' などに引用符を付ける。noCompatMode: true で外れる。
  • 確実なのは、文字列にしたい値を引用符で囲むこと。8 進数は綴りがパーサーごとに違うので、文字列で受けてアプリ側で変換する。

参考

  • YAML Ain’t Markup Language Version 1.2.2(10.3 Core Schema、1.2 節)
  • yaml.org Boolean Language-Independent Type for YAML Version 1.1 / Integer Language-Independent Type for YAML Version 1.1
  • js-yaml README(noCompatMode、スキーマ一覧)と CHANGELOG(4.0.0、1.0.0)
  • PyYAML(PyPI のプロジェクト説明)

YAMLのNOやonがfalseになる js-yaml 4とPyYAMLの差を実測
https://blog.hashito.biz/2026/10/28/yaml-norway-problem-no-on-0755-js-yaml-4-pyyaml-yaml11-12/
著者
hashito
作成日
2026年10月28日
著作権

このバージョンはその範囲に入るのか

依存の話でいちばん間違えやすいのは ^1.2.3 や ~1.2.3 がどこまでを許すかです(^0.2.3 のようにメジャーが 0 のときは規則が変わります)。範囲とバージョンを貼ると、下限・上限に展開したうえで一致・不一致とその理由を表示するsemver 範囲判定ツールを置いています。ブラウザの中だけで動き、入力はどこにも送信しません。