経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

RFC8259 の「Implementations MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text」について

プログラマのための文字コード技術入門』の p.216 にある、
JSONでは、データ先頭にBOMをつけないことが求められています(RFC8259)」
についてちょっとだけ詳しく調べる。

RFC8259の該当箇所は8.1. Character Encodingで、
「Implementations MUST NOT add a byte order mark (U+FEFF) to the beginning of a networked-transmitted JSON text」
とある。
続いて
「In the interests of interoperability, implementations that parse JSON texts MAY ignore the presence of a byte order mark rather than treating it as an error」
とあるように、相互運用性の観点から、BOMがあればエラーとして扱うのではなく、無視してもいいらしい。

BOM(Byte Order Mark)とは、ビッグエンディアンかリトルエンディアンのどちらのバイト順を採用しているかを示すために、データの先頭に付ける印のこと。
U+FEFFの符号位置を用い、ビッグエンディアンではFE FF、リトルエンディアンではFF FEという2バイトの列になる。
符号位置は、文字コード表の中の位置のこと。
ビッグエンディアンは、上位8ビットが先頭にくるバイト順、リトルエンディアンは、下位8ビットが先頭にくるバイト順のこと。
U+FEFFU+は、Unicodeの符号位置を表すのに付ける接頭辞。

16ビットのデータを8ビット単位のバイト列にするときには、ビッグエンディアンかリトルエンディアンのどちらを採用するかの問題がある。
Unicodeの符号化方式の中の1つであるUTF-16は、16ビット単位であるためこの問題が発生し、解消するためにBOMを付けることがある。

JSONでBOMを付けないとなると、バイト順の区別はどうするのだろうと思ったところ、同じ RFC8259 8.1. Character Encoding にこう書いてあった。
JSON text exchanged between systems that are not part of a closed ecosystem MUST be encoded using UTF-8
UTF-16を扱わないのにRFCなぜBOMに言及しているのだろうと思ったところ、『プログラマのための文字コード技術入門』の p.154 にこう書いてあった。
「バイト順が問題になることのないUTF-8にはBOMは本来関係がありません。ところが、BOMをUTF-8で表現した3バイトの値(EF BB BF)が、UTF-8のデータ列の先頭についていることがあります。バイト順の印という元々の意味を離れて、UTF-8のデータであることの印として利用価値があるという考えもあります。」

ネットワーク転送されるJSONにはその利用価値は必要なく、相互運用性に支障をきたすから、「Implementations MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text」とされている。



BOMに関する実際の問題の例として、node-fetchのHandling a BOM with .json() #541というissueにある、レスポンスをJSONとして取得する際にエラーが起こるという問題がある。
node-fetchの`.json()`には内部的に`JSON.parse`が用いられており、引数のtextにBOMが含まれていれば、そこでエラーが生じてしまう。JSONの構文はRFC8259に準拠しているからだ。
ユーザー側の解決例としてはここにあるように、textとして取得してから、BOMに当たる0xFEFFを除去し、JSONにする例がある。
node-fetch側のリリースされている解決法は、fix: handle bom in text and json #1482にあるように、内部でbuffer.toString()ではなくTextDecoder().decode()を使うようにしている。
buffer.toString()では、BOMに当たる0xFEFFがあったとしてもそのまま文字列化してしまい、それをJSON.parseするためエラーが生じる。
TextDecoder()のパラメータであるutfLabelのデフォルトは"utf-8"
そして重要なのがデフォルトがfalseTextDecoder.ignoreBOM
命名がややこしいが、TextDecoder.prototype.ignoreBOM not working as expectedの回答に
「If on the other hand you want it to be removed from the output, then leave it as false, the parser will treat it specially, and remove it from the output」
とあるように、TextDecoder().decode(buffer)の結果にはBOMが含まれなくなり、問題なくJSON.parse(text)ができる。


このように目に見えないBOMと戦ってJSONが生成される例がある。