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 | |
実行環境は 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 | |
1 | |
既定では '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 | |
上の sample.yaml を保存し、Node.js 用の load.js を作る。
1 | |
Python 用の load.py も作る。
1 | |
1 | |
1 | |
1 | |
2. 文字列のつもりの値は引用符で囲む
どちらのパーサーでも同じ結果にする一番確実な方法は、文字列として扱いたい値を引用符で囲むことだ。fixed.yaml を作る。
1 | |
1 | |
1 | |
1 | |
両方とも全て文字列になり、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 | |
1 | |
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 | |
つまり、このツールの結果は 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 のプロジェクト説明)
このバージョンはその範囲に入るのか
依存の話でいちばん間違えやすいのは ^1.2.3 や ~1.2.3 がどこまでを許すかです(^0.2.3 のようにメジャーが 0 のときは規則が変わります)。範囲とバージョンを貼ると、下限・上限に展開したうえで一致・不一致とその理由を表示するsemver 範囲判定ツールを置いています。ブラウザの中だけで動き、入力はどこにも送信しません。