JSON.parseがBOMで失敗する原因と対処|PowerShellで作ったJSONが読めない理由
JSONの中身を目で見る限り正しいのに、JSON.parse が Unexpected token で失敗する。エラーに出ている先頭の文字は空白のように見えるか、何も表示されない。この症状の代表的な原因が BOM(Byte Order Mark) です。
結論から言うと、ファイルの先頭にある U+FEFF(UTF-8では EF BB BF の3バイト)を取り除けば直ります。 この記事では、どこでBOMが付くのか、どう確認するのか、どう除去するのかを、Node.js v20 と Windows PowerShell 5.1 で実際に試した結果とあわせて整理します。
BOMが原因かを最短で見分ける方法
BOMは目に見えないため、エディタで開いても気づけません。次の3点で切り分けます。
| 確認すること | BOMの可能性が高い状態 |
|---|---|
| エラーが指す位置 | 先頭(1文字目)。ただし後述のとおり、最近のNodeは位置を表示しません |
| 先頭の1文字の文字コード | 0xFEFF(65279) |
| ファイルの先頭3バイト | EF BB BF |
Node.js で先頭を確認するコードは次のとおりです。
const fs = require("fs");
const text = fs.readFileSync("data.json", "utf8");
console.log(text.charCodeAt(0).toString(16)); // "feff" ならBOMあり
console.log(fs.readFileSync("data.json").subarray(0, 3).toString("hex")); // "efbbbf" ならBOMあり
エラーの文面だけで判断しようとすると、後述の落とし穴にはまります。「先頭1文字の文字コードを出す」のが最も確実です。
実測:BOM付きJSONを読むとどうなるか
次の内容のJSONを、BOM付きUTF-8で保存して Node.js v20.20.1 で読み込みました。
{"name":"太郎","age":20}
| 読み込み方法 | 結果(実測) |
|---|---|
JSON.parse(fs.readFileSync(path, "utf8")) | 失敗。Unexpected token '', "{"name":""... is not valid JSON |
require("./bom.json") | 成功 |
new Response(buffer).json()(fetch の res.json() と同じ系統) | 成功 |
先頭の を除去してから JSON.parse | 成功 |
new TextDecoder().decode(buffer) | BOMが除去される(文字数22) |
new TextDecoder("utf-8", { ignoreBOM: true }).decode(buffer) | BOMが残る(文字数23) |
buffer.toString() / readFileSync(path, "utf8") | BOMが残る(文字数23) |
この表から分かることが2つあります。
- 同じファイルでも、読み方によって成功したり失敗したりします。
requireでは動いていたJSONが、fs.readFileSyncに書き換えた途端に壊れる、という現象が起こりえます TextDecoderは初期設定でBOMを除去します。fsの"utf8"読み込みは除去しません
「APIから取得したときは動くのに、ローカルのファイルを読むと壊れる」という症状は、この差で説明できる場合があります(ただし他の原因もあるため、先頭の文字コードで確認してください)。
他所ではあまり書かれていない落とし穴
落とし穴1:最近のNodeは「position 0」と表示しない
古い解説の多くは「エラーが position 0 を指していればBOM」と書いています。しかし今回の Node v20.20.1 のエラー文は次の形式でした。
Unexpected token '', "{"name":""... is not valid JSON
位置の数字は出ず、問題の文字と、先頭付近の抜粋が表示されます。 「position 0 が出ないからBOMではない」と判断すると見落とします。エラー文の中の Unexpected token の直後のクォートの中身が、見た目は空に見える場合はBOMを疑ってください。
なお、エラー文の形式はNodeのバージョンやブラウザによって異なります。ここで確認できたのは v20.20.1 のみで、他の環境での文面は未確認です。
落とし穴2:PowerShell 5.1 の -Encoding utf8 はBOM付きで保存する
「BOMなしのUTF-8で保存したはずなのにBOMが付く」場合、PowerShell が原因のことがあります。Windows PowerShell 5.1(powershell.exe)で、同じ文字列を4通りの方法で保存し、先頭バイトを調べました。
| 保存方法(PowerShell 5.1.26100) | 先頭バイト(実測) | 意味 |
|---|---|---|
Out-File a.json -Encoding utf8 | EF BB BF | BOM付きUTF-8 |
Set-Content b.json -Encoding utf8 | EF BB BF | BOM付きUTF-8 |
Out-File c.json(エンコード指定なし) | FF FE | UTF-16LE(BOM付き) |
[IO.File]::WriteAllText("d.json", $text) | 7B({) | BOMなしUTF-8 |
-Encoding utf8 と書いても、5.1では「BOMなし」にはなりません。 BOMなしで保存したい場合は、[IO.File]::WriteAllText を使うか、PowerShell 7以降(pwsh)を使う方法が考えられます。PowerShell 7 の挙動は今回は未検証です。
さらに見落としやすいのが、エンコード指定なしの Out-File がUTF-16LEで保存する点です。これをNodeに "utf8" で読ませると、BOMとは別の壊れ方をします。
落とし穴3:UTF-16で保存されたJSONは、BOMを除いても直らない
上のUTF-16LEのファイル(c.json)を Node で読むと、次のようになりました。
| 読み方 | 結果(実測) |
|---|---|
"utf8" で読む | Unexpected token '�' のように文字化けした記号で失敗 |
"utf16le" で読む | 先頭に U+FEFF が残り、Unexpected token '' で失敗 |
つまり、UTF-16で保存されていると、まず文字コードを合わせて読み、そのうえでBOMを除去する、という2段階が必要です。「BOMを消せば直る」という手順だけでは、このケースは直りません。エラー文に �(文字化けの記号)が出ていたら、BOMではなく文字コード自体が違う可能性を疑います。
落とし穴4:CSVのBOMは「最初のキーだけ」壊す
ExcelでCSV UTF-8として保存したファイルはBOM付きになります。これを BOM を除去せずに分割すると、1列目の見出しが次のようになります。
const head = csvText.split("\r\n")[0].split(",");
head[0] === "name"; // false
head[0].length; // 5("name" は4文字のはずが5文字)
実測では、1列目のキーが "name"(先頭に不可視文字が付いた5文字)になり、obj.name で値が取れなくなりました。エラーは出ずに値が undefined になるため、気づきにくい症状です。CSVとJSONの変換については CSVをJSONに変換する方法 でも触れています。
BOMを取り除く方法
Node.js:読み込み時に除去する
const fs = require("fs");
function readJson(path) {
const text = fs.readFileSync(path, "utf8").replace(/^/, "");
return JSON.parse(text);
}
先頭の1文字だけを除去する正規表現です。BOMがなければ何も変わらないので、常に通して問題ありません。ブラウザの fetch で取得する場合は、前述の表のとおり res.json() がBOMを許容した例がありますが、res.text() で取得して自分で JSON.parse するなら、同じ除去処理を入れてください。
PowerShell:BOMなしで書き出す
$json = $data | ConvertTo-Json
[System.IO.File]::WriteAllText("$PWD\data.json", $json)
Windows PowerShell 5.1 では、この書き方でBOMなし(実測の d.json と同じ結果)になります。$PWD\ と付けているのは、.NET側の「現在のディレクトリ」がPowerShellの場所と一致しないことがあるためです。
エディタ:保存し直す
VS Code なら、右下の文字コード表示(UTF-8 with BOM)をクリックし、「エンコード付きで保存」から UTF-8 を選びます。メモ帳は新しいWindowsではBOMなしが初期値と考えられますが、古い版ではBOM付きが初期値でした。環境によって異なるため、保存形式の表示を確認してください。
既存ファイルが大量にある場合
多数のJSONを一括で直す必要があるときは、除去ではなく読み込み側で除去するほうが安全です。ファイルを書き換えると、Gitの差分やビルドキャッシュに影響します。
BOMを付けてよいのか、付けないほうがよいのか
JSONの仕様(RFC 8259)は、JSONテキストの先頭にBOMを付けてはならない、としています。JSONに限れば、BOMなしで保存するのが原則と考えられます。
一方で、ExcelでCSVを開いて日本語を正しく表示させたい場合には、BOM付きUTF-8のほうが都合がよい場面があります。JSONとCSVで事情が違うため、「BOMは悪」と一括りにせず、渡す相手がJSONパーサーか、Excelかで決めるのが実務的です。
JSONが壊れているのか確認したいとき
BOMを除去しても失敗するなら、別の原因(末尾カンマ、シングルクォート、コメントなど)があります。貼り付けたJSONの構文は JSON整形・検証ツール でエラーメッセージを確認できます。原因の一覧は JSONが読み込めない・整形できない原因と対処法 にまとめています。
今回確認していないこと
- ブラウザ(Chrome・Firefox・Safari)での
JSON.parseのエラー文面 - Python、Java、Go など他言語の標準ライブラリの挙動
- PowerShell 7 以降の既定エンコード
- Node.js v20.20.1 以外のバージョン
これらは未検証です。お使いの環境で異なる結果になる場合があります。
まとめ
JSON.parseが先頭で失敗し、問題の文字が見えないときは U+FEFF(BOM) を疑う。先頭1文字の文字コードを出して確認する- 最近のNodeのエラー文には
position 0が出ない。位置の数字がなくてもBOMの可能性は残る - 同じファイルでも
requireやResponse.json()は通り、fs.readFileSync+JSON.parseは失敗する場合がある - PowerShell 5.1 の
-Encoding utf8はBOM付き、エンコード指定なしのOut-Fileは UTF-16LE。BOMなしにするなら[IO.File]::WriteAllTextを使う - エラー文に
�が出るなら、BOMではなく文字コード違い(UTF-16など)を疑う。この場合は文字コードを合わせて読んでから除去する