Web便利ツール集

JSON.parseがBOMで失敗する原因と対処|PowerShellで作ったJSONが読めない理由

JSONBOM文字コードPowerShellNode.js

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つあります。

  1. 同じファイルでも、読み方によって成功したり失敗したりします。 require では動いていたJSONが、fs.readFileSync に書き換えた途端に壊れる、という現象が起こりえます
  2. 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 utf8EF BB BFBOM付きUTF-8
Set-Content b.json -Encoding utf8EF BB BFBOM付きUTF-8
Out-File c.json(エンコード指定なし)FF FEUTF-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 以外のバージョン

これらは未検証です。お使いの環境で異なる結果になる場合があります。

まとめ

  1. JSON.parse が先頭で失敗し、問題の文字が見えないときは U+FEFF(BOM) を疑う。先頭1文字の文字コードを出して確認する
  2. 最近のNodeのエラー文には position 0 が出ない。位置の数字がなくてもBOMの可能性は残る
  3. 同じファイルでも require や Response.json() は通り、fs.readFileSync + JSON.parse は失敗する場合がある
  4. PowerShell 5.1 の -Encoding utf8 はBOM付き、エンコード指定なしの Out-File は UTF-16LE。BOMなしにするなら [IO.File]::WriteAllText を使う
  5. エラー文に � が出るなら、BOMではなく文字コード違い(UTF-16など)を疑う。この場合は文字コードを合わせて読んでから除去する

この記事で使うツール

関連記事