UTF-8 の BOM(EF BB BF):見えない3バイトの扱いが規格ごとに違う理由
UTF-8 ファイル先頭の EF BB BF が、ブラウザーでは読み飛ばされ JSON.parse ではエラーになる理由を、Unicode、RFC 3629、RFC 8259(JSON)、WHATWG の Encoding・HTML 標準の規定から整理しました。
UTF-8 ファイルの先頭に EF BB BF の3バイトがあると、何事もなく読むプログラムもあれば、1文字目で止まるパーサーもあります。この3バイトが BOM(byte order mark)です。
反応が分かれるのは、たいてい実装ミスのせいではありません。規格ごとに BOM に与えた役割が違います。JSON エラーのまとめでは1段落だけ触れましたが、ここでは規格別に詳しく見ます。
BOM は U+FEFF という1文字
BOM は文字 U+FEFF をデータの先頭に置いたものです。そのため、符号化方式によってバイトが変わります。
| 符号化方式 | BOM のバイト |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16 ビッグエンディアン | FE FF |
| UTF-16 リトルエンディアン | FF FE |
| UTF-32 ビッグエンディアン | 00 00 FE FF |
| UTF-32 リトルエンディアン | FF FE 00 00 |
UTF-16 や UTF-32 ではバイト順を示します。UTF-8 のバイト順は1通りしかないため、その役割はありません。Unicode の FAQ は、UTF-8 の BOM を「これは UTF-8 である」というシグネチャーとして説明しています。Unicode 標準 18.0 は、UTF-8 に BOM は必須ではなく、使用を推奨も非推奨もしないとしています。
先頭以外では内容の一部
U+FEFF の名前は ZERO WIDTH NO-BREAK SPACE です。以前は単語間の改行を防ぐために使われていました。現在の Unicode は、その用途には U+2060 WORD JOINER を推奨しています。BOM と混同されないためです。
先頭以外にある U+FEFF はシグネチャーではなく、内容の一部です。RFC 3629 も、先頭以外の U+FEFF をシグネチャーとして解釈してはならないと定めています(MUST NOT)。
先頭の U+FEFF を認めるかどうかについて、RFC 3629 は条件を分けています。
- 常に UTF-8 と定められたプロトコル要素では、シグネチャーを禁止すべきです(SHOULD)。
- 符号化方式を示す仕組みがあり、実装がそれを常に正しく使うと見込める要素でも禁止すべきです(SHOULD)。
- その仕組みがなく、禁止を徹底できない要素では禁止すべきではありません(SHOULD NOT)。
- 仕組みがあっても、実装が常に正しく使うとは見込めない要素でも禁止すべきではありません(SHOULD NOT)。
- シグネチャーを禁止した要素では、先頭の U+FEFF も幅なし改行禁止スペースとして解釈しなければなりません(MUST)。
RFC は MIME エンティティのような大きな要素について、取得元と符号化方式の表示がどれだけ信頼できるかにより、2つの SHOULD NOT のどちらにも該当し得ると説明しています。
JSON:付けてはならず、無視するかは任意
RFC 8259 の 8.1 節によると、閉じた環境の外で交換する JSON は UTF-8 でなければなりません(MUST)。ネットワークで送る JSON に BOM を付けてはなりません(MUST NOT)。一方、パーサーは相互運用のために BOM を無視してもかまいません(MAY)。
無視するかどうかは実装次第です。JSON で空白として認められるのはスペース、タブ、LF、CR だけです。BOM を読み飛ばさないパーサーにとって U+FEFF は不正な先頭文字です。
JSON.parse('\uFEFF{"a":1}')
// SyntaxError
この動作は執筆日に Node.js v24.13.0 で確認しました。
Python の公式ドキュメントによると、json モジュールはシリアライズ時に BOM を付けず、BOM で始まる文字列をデシリアライズすると ValueError を送出します。バイト列をデコードする場合、'utf-8' は先頭の U+FEFF を文字として残し、'utf-8-sig' は先頭3バイトを読み飛ばします。
ブラウザーの fetch() は別の経路を通ります。Fetch 標準では Response.text() と Response.json() が UTF-8 デコードを行い、その段階で先頭の BOM が取り除かれます。同じファイルでも、fetch では読めるのに、BOM を残す方法で文字列にしてから JSON.parse に渡すと失敗することがあります。
Web では BOM を最初に確認する
WHATWG Encoding 標準のデコード処理は、本文を読む前に EF BB BF、FE FF、FF FE を確認します。見つかれば、その符号化方式を採用して BOM を取り除きます。
HTML の文字コード判定も最初に BOM を確認します。見つかった場合の信頼度は certain です。HTTP Content-Type の charset や <meta> 宣言はその後に調べます。
ここでは文書作成上の要件とブラウザーの処理規則を分ける必要があります。HTML に適合する文書の実際の符号化方式は UTF-8 でなければなりません。一方、ブラウザーの処理規則には Shift_JIS、EUC-KR、windows-1252 など、従来の符号化方式をデコードする手順も残っています。
文書が BOM で始まらず、Content-Type にも符号化方式がなく、iframe の srcdoc 文書でもない場合、作成者は <meta charset> または同等の http-equiv 宣言で符号化方式を示す必要があります。この宣言は先頭 1024 バイト以内に置かなければなりません。
TextDecoder のオプション名は少し紛らわしいものです。既定値の ignoreBOM: false では BOM を認識して結果から取り除きます。true にすると U+FEFF が出力文字列に残ります。
まとめ
| 規格・実装 | BOM の扱い |
|---|---|
| Unicode 標準 18.0 | UTF-8 では必須でなく、推奨も非推奨もしない |
| RFC 3629 | 常に UTF-8 の要素などでは禁止を推奨 |
| RFC 8259 | ネットワーク JSON には付加禁止。パーサーによる無視は任意 |
| WHATWG Encoding・HTML の処理規則 | ラベルより先に確認して取り除く |
| HTML の作成規則 | 適合する文書の実際の符号化方式は UTF-8 |
JSON.parse、Python json |
文字列先頭の U+FEFF をエラーとして扱う |
ネットワーク JSON のように UTF-8 をすでに必須としている、または宣言している形式では、その形式や受信側のツールが BOM を明示的に求めていない限り、BOM を付けないでください。
すでに BOM が付いたデータを受け取る場合は、文字列にした後で削除するより、バイト列をデコードする段階で処理します。
先頭文字で原因不明のエラーが出たら、エディターの表示ではなくバイトを確認できます。
od -An -tx1 -N3 ファイル名
出力が ef bb bf で始まれば、そのファイルには UTF-8 BOM があります。