API が突然 500 を返し始めて、ログを見ると SyntaxError: Unexpected token < in JSON at position 0。「JSON が壊れてる?いや待って俺が壊したのか?」あの絶望感、よくわかります。JSON のエラーメッセージは親切ではありません。エラーが起きた「位置」は教えてくれても、「なぜ」は教えてくれない。本記事は、API レスポンスや設定ファイルで JSON エラーに遭遇したとき、上から順にチェックすれば 9 割の原因にたどり着けるチェックリストです。残り 1 割のために「診断フロー」セクションも用意しました。
JSON(JavaScript Object Notation)は 2001 年ごろに Douglas Crockford が提唱し、2017 年に RFC 8259 として標準化されました。設計のゴールは「最小・相互運用可能・言語非依存」。XML の複雑さへの反動として生まれたため、仕様はあえて小さく保たれています。コメントも末尾カンマも省略構文もすべて「言語依存の曖昧さ」として意図的に排除されました。eval() で安全にパースできるサブセットであることも当初の目標でした。この厳格さがあるからこそ、どの言語のパーサも同じ結果を返せます。
エラーメッセージ早見表
パーサが返すエラーメッセージは実装によって表現が異なりますが、パターンはほぼ決まっています。まずここで目星をつけてから、該当セクションに飛んでください。
| エラーメッセージ(抜粋) | 最有力な原因 | 参照 |
|---|---|---|
Unexpected token , | 末尾カンマ または 連続カンマ | 1 |
Unexpected token ' | シングルクォート使用 | 2 |
Expecting property name enclosed in double quotes | キー未クォート または シングルクォート | 3, 2 |
Unexpected token / | コメント(// または /* */) | 4 |
Unexpected end of JSON input / Unexpected end of input | 括弧の閉じ忘れ または 途中で切れたレスポンス | 5 |
Bad escaped character / Invalid \escape | エスケープ忘れ(特に Windows パス) | 6 |
Unexpected number | 数値の先頭 0 | 7 |
Unexpected token N / Unexpected token I | NaN または Infinity | 8 |
Unexpected token(不可視文字) | BOM または 不可視スペース | 9, 10 |
エラー位置が line 1 column 1 | BOM、または空文字列・HTML が来ている | 9 |
末尾カンマ(trailing comma)
JSON パーサが吐く典型的なメッセージは次のとおりです。
- Node.js:
SyntaxError: Unexpected token } in JSON at position 15 - Python:
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 16 (char 15) - ブラウザ DevTools:
JSON.parse: unexpected character at line 1 column 16 of the JSON data
エラー位置を見ると「閉じ括弧の直前」を指しているのに気づくはずです。それが末尾カンマのサインです。
{
"name": "Alice",
"age": 30,
}{
"name": "Alice",
"age": 30
}なぜ弾かれるか: RFC 8259 のオブジェクト文法は value = false / null / true / object / array / number / string と定義されており、カンマの後には必ず次の要素が続く前提です。カンマで終わることを認める文法規則が存在しません。
よくある発生源: JavaScript/TypeScript のオブジェクトリテラルをそのままコピーしたとき。ESLint の comma-dangle: always-multiline ルールを適用した JS コードをコピペすると末尾カンマが自動で付いています。また Python の dict を str() や pprint で出力してもこの問題は起きませんが、手動で JSON 風に書くと末尾カンマを入れてしまいがちです。
VS Code では .vscode/settings.json を "json.validate.enable": true にしておくと、ファイルを保存する前に末尾カンマを赤波線で教えてくれます。設定ファイルを手書きするプロジェクトでは必ず有効にしておきましょう。
シングルクォート
JSON では文字列もキーも 必ずダブルクォート で囲みます。シングルクォートは JSON の文法に存在しません。Node.js は SyntaxError: Unexpected token ' in JSON at position 0、Python は json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) を返します。
{'name': 'Alice', 'city': "Tokyo"}{"name": "Alice", "city": "Tokyo"}なぜ弾かれるか: JSON の文字列定義は "(U+0022)で始まり " で終わる文字シーケンスのみです。シングルクォート(U+0027)は文字列を開始する構文要素として認められていません。
よくある発生源: Python の dict をそのまま print() したときが最多です。Python は文字列をシングルクォートで表示するため、{'key': 'value'} という出力が得られます。これをそのままログから貼り付けると即エラーです。正しくは import json; print(json.dumps(data)) で出力してください。Ruby の Hash#inspect でも同様の問題が起きます。
JSON.parse() の代わりに eval() を使うとシングルクォートが通ってしまいます。しかし eval は任意コード実行の脆弱性になるため、絶対に使わないでください。サードパーティ製の JSON を eval で処理しているレガシーコードは今すぐ修正を。
キーがクォートされていない
JavaScript のオブジェクトリテラルは {key: "value"} のようにキーをクォートなしで書けますが、JSON ではキーも必ずダブルクォートで囲む必要があります。Python で言えば json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes がこれを示しています。
{name: "Alice", age: 30}{"name": "Alice", "age": 30}なぜ弾かれるか: JSON の object 定義は { string : value } であり、キー部分は必ず string(ダブルクォート囲み)でなければなりません。JavaScript エンジンが許容する「識別子をキーとして使う」構文は JSON には存在しません。
よくある発生源: JavaScript のコードスニペットや設定例をコピーしたとき。特に StackOverflow の古い回答や README サンプルに {port: 3000, host: "localhost"} のような JS オブジェクト表記が混在しています。Node.js の require('./config.json') で読む設定ファイルを JS オブジェクト表記で書いて気づかないケースも多いです。
コメントが入っている
JSON 仕様には // や /* */ のコメント構文がありません。コメントを含む JSON をパースすると、Node.js では SyntaxError: Unexpected token / in JSON at position 12 のようにスラッシュを異物として報告します。
{
// データベース設定
"host": "localhost",
"port": 5432 /* デフォルトポート */
}{
"host": "localhost",
"port": 5432
}なぜ弾かれるか: Douglas Crockford は後のインタビューで「JSON からコメントを外したのは、人々がパース指示のコメントを入れ始め、相互運用性が壊れるのを防ぐためだった」と語っています。設計上の意思決定であり、単純な見落としではありません。
よくある発生源: VS Code の settings.json や tsconfig.json は JSONC(JSON with Comments)形式を採用しているため、コメントが書けます。これを他のパーサ(例: JSON.parse() や Python の json.load())で読もうとすると即エラーです。また ESLint の .eslintrc.json は実は JSONC で処理されています。
設定ファイルでコメントを残したい場合の選択肢は3つあります。① JSON5 または JSONC を採用してパーサも対応版に変える。② "_comment": "ここは何のフィールド" という専用キーを使う(慣習的な回避策)。③ YAML や TOML に移行する(コメントが仕様に含まれる)。
括弧の対応関係が崩れている
ネストが深い JSON で最も見つけにくいバグです。{ と }、[ と ] の数が合っていても、位置が間違っていれば壊れます。エラーメッセージは Unexpected end of JSON input(閉じが足りない)か Unexpected non-whitespace character after JSON data(閉じが多い)になります。
{
"users": [
{"name": "Alice"},
{"name": "Bob"}
}
]{
"users": [
{"name": "Alice"},
{"name": "Bob"}
]
}なぜ起きるか: 手書きや文字列連結で JSON を組み立てると、コピペ時に閉じ括弧の順序が入れ替わります。特に「配列の最後の要素を削除したとき」に括弧だけ残るパターンが多いです。
よくある発生源: API レスポンスをログに出力したとき、ログ行数の上限でレスポンスが途中で切れることがあります。この場合 Unexpected end of JSON input が出ますが、自分のコードに問題はなく、ログ設定かネットワーク層を疑うべきです。また、HTTP ストリームが途中で切断された場合も同じエラーになります。
コマンドラインなら python3 -m json.tool input.json が最速です。整形に成功すれば有効な JSON、失敗すればエラー行番号と列番号が出ます。jq . も同様で、括弧ミスには特に明確なエラーメッセージを返します。
文字列内のエスケープ忘れ
JSON 文字列内で特別な意味を持つ文字はエスケープが必須です。エラーメッセージは Bad escaped character in JSON や Invalid \escape(Node.js)として現れます。
エスケープが必要な文字の一覧:
\"— ダブルクォート\\— バックスラッシュ\/— スラッシュ(オプション、省略可能)\n— 改行(LF)\r— 復帰(CR)\t— タブ\uXXXX— Unicode コードポイント
{"path": "C:\Users\Alice\Documents"}{"path": "C:\\Users\\Alice\\Documents"}なぜ弾かれるか: \U・\A・\D は JSON の定義済みエスケープシーケンスではないため、パーサは「不正なエスケープ」として拒否します。JavaScript の文字列では \U は単に U として扱われますが、JSON パーサはより厳格です。
よくある発生源: Windows ユーザーがファイルパスをそのまま JSON に貼り付けたとき。また JS の template literal 内で改行を含む文字列を作り、それを JSON 文字列値としてそのままシリアライズしようとしたときも同様です。実際の改行文字(U+000A)は JSON 文字列の中に直接置けないため、\n に変換する必要があります。
JSON.stringify() は自動でエスケープを行いますが、文字列を手で組み立てている場合は自力でエスケープが必要です。Node.js の JSON.stringify() を使えるのに使わず、テンプレートリテラルで JSON を組み立てているコードは今すぐリファクタリングしてください。
数値の先頭 0
0123 のように数値リテラルが 0 で始まると JSON では不正です。Node.js は SyntaxError: Unexpected number in JSON at position 6、Python は json.decoder.JSONDecodeError: Extra data: line 1 column 4 (char 3) を返します(0 単体はパースされ、123 が余分なデータとして扱われます)。
{"zip_code": 01234, "id": 007}{"zip_code": "01234", "id": "007"}なぜ弾かれるか: 歴史的に 0 で始まる数値リテラルは8進数として解釈される言語(C、Java、JavaScript の旧仕様)があり、相互運用上の曖昧さを生みます。JSON はこの曖昧さを仕様レベルで排除するため、0 単体か 0. 以下の小数点数以外で 0 から始まる数値を禁止しています。
よくある発生源: 郵便番号・電話番号・社員番号など、「数字だが先頭 0 が意味を持つ」フィールドを数値型で格納しようとしたとき。データベースから取得した NUMERIC 型フィールドを、そのまま JSON に入れるケースで踏みます。これらのフィールドは常に文字列として JSON に入れるのが正解です。
NaN / Infinity / undefined
JSON の数値型は IEEE 754 の有限な数のみを表現します。NaN・Infinity・-Infinity・undefined はサポート外です。Node.js では SyntaxError: Unexpected token N in JSON at position 8(NaN の場合)または SyntaxError: Unexpected token I in JSON at position 8(Infinity の場合)が出ます。
{"score": NaN, "ratio": Infinity, "val": undefined}{"score": null, "ratio": null, "val": null}なぜ弾かれるか: NaN と Infinity は JavaScript の識別子であり、JSON の value に識別子は存在しません(true/false/null という3つのキーワードだけが例外として定義されています)。
よくある発生源: Python の json.dumps() はデフォルトで float('nan') を NaN として出力します(RFC 違反ですが Python 仕様)。これを JavaScript 側で JSON.parse() するとクラッシュします。対処は json.dumps(data, allow_nan=False) で例外を出すか、simplejson ライブラリを使って null に変換するかです。JavaScript の JSON.stringify() は NaN・Infinity・undefined を黙って null に変換します。受け取り側が気づかずに処理を続けることがあるため、送信前にバリデーションを挟むことを推奨します。
npm パッケージ json-stringify-safe は循環参照を含むオブジェクトを安全に文字列化しますが、NaN や Infinity の扱いは通常の JSON.stringify() と同じです。NaN を安全に扱いたい場合は serializer 関数で明示的に変換してください。
UTF-8 BOM が混入している
BOM(Byte Order Mark、U+FEFF)が JSON ファイルの先頭に混入していると、パーサから見た最初の文字が { ではなく \ufeff になります。結果として SyntaxError: Unexpected token in JSON at position 0(トークン部分が空白や文字化けに見える)や、Python の json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) が出ます。エラー位置が「line 1 column 1」なら、まず BOM を疑ってください。
EF BB BF 7B 22 6E 61 6D 65 22 3A 22 41 6C 69 63 65 22 7D
→ {"name":"Alice"}7B 22 6E 61 6D 65 22 3A 22 41 6C 69 63 65 22 7D
→ {"name":"Alice"}なぜ起きるか: Windows の「メモ帳」やいくつかの Excel エクスポート機能は、デフォルトで UTF-8 BOM 付きでファイルを保存します。UTF-16 には BOM が必須ですが、UTF-8 での BOM は「あってもよい」程度の存在であり、多くの JSON パーサは対応していません(RFC 8259 では UTF-8 BOM を明示的に禁止しています)。
よくある発生源: Excel でデータを CSV に書き出した後、それを加工して JSON にしたとき。Windows 端末でメモ帳を使って設定ファイルを編集したとき。CI/CD 環境では問題ないが、手元の Windows 環境でだけエラーになる、という症状ならまず BOM を疑います。
検出と除去: Linux/Mac なら file config.json コマンドで UTF-8 Unicode (with BOM) text と表示されます。除去は sed -i '1s/^\xEF\xBB\xBF//' config.json(macOS は sed -i '' '1s/...')または VS Code でファイルを開き、右下のエンコーディング表示をクリックして「UTF-8 (BOM なし)」で保存し直します。
Node.js の fs.readFileSync(path, 'utf8') は BOM を自動除去しません。fs.readFileSync(path, 'utf8').replace(/^\uFEFF/, '') で先頭の BOM を取り除いてから JSON.parse() に渡すか、strip-bom パッケージを使ってください。
改行コードや見えない空白
JSON 文字列値の中に制御文字(U+0000〜U+001F)が直接含まれると不正です。また、非破壊スペース(U+00A0)やゼロ幅スペース(U+200B)、ゼロ幅非ジョイナー(U+200C)などが JSON のキーや値の周辺に混入すると、見た目は正常でもパーサが拒否します。エラーメッセージは Invalid character や Unexpected token(トークン部分が空白に見える)として出ます。
{"message": "Hello
World"}{"message": "Hello\nWorld"}なぜ起きるか: JSON の文字列定義では、エスケープされていない制御文字(U+0000〜U+001F)を文字列内に直接置くことを禁止しています。改行(U+000A)も制御文字なので、\n としてエスケープする必要があります。
よくある発生源: Web フォームから送信されたテキストを JSON に直接埋め込んだとき。特に <textarea> の内容はユーザーが自由に改行を入れられるため、バックエンドで JSON.stringify() を通さずに文字列連結で JSON を組み立てると改行が生で入ります。また、Wikipedia や Google Docs からコピーしたテキストには非破壊スペースが含まれていることが多く、これがキー名に混入すると「キーが存在するのに undefined が返る」という症状が出て非常に気づきにくいです。
不可視文字の検出には cat -A file.json(Linux)や hexdump -C file.json | head が有効です。Node.js なら [...str].map(c => c.codePointAt(0).toString(16)) でコードポイント一覧を出せます。キー名のタイポではなく不可視文字が原因の場合、この方法でしか発見できないことがあります。
診断フロー:チェックリストで解決しないとき
10 項目を全部チェックしても原因が分からない場合、以下の手順で絞り込みます。
Step 1: JSON を最小化する
問題の JSON が大きい場合、半分に切ってそれぞれをパースし、エラーが出る側をさらに半分に切る。これを繰り返せば、巨大な JSON でも数回のイテレーションで問題箇所を特定できます。「バイナリサーチ」の考え方です。
Step 2: 複数のパーサで試す
エラーメッセージの表現はパーサによって異なります。Node.js の JSON.parse()、Python の json.loads()、jq の3つで試すと、より詳細な位置情報を得られることがあります。特に jq は行番号・列番号の精度が高く、複数行の JSON に対して有用です。
# jq でエラー位置を確認
echo '{"a": 1,}' | jq .
# Python で詳細なエラー情報を得る
python3 -c "import json; json.loads(open('file.json').read())"
# Python json.tool で整形しながら検証
python3 -m json.tool file.jsonStep 3: バイト単位でダンプする
エラー位置が「position 0」や「column 1」など先頭付近を指している場合、BOM や不可視文字が原因の可能性が高いです。xxd や hexdump でファイル先頭のバイト列を確認します。
# ファイル先頭 32 バイトを16進数で確認
xxd file.json | head -2
# UTF-8 BOM は先頭が EF BB BF
# 期待値: 7B 22 ...({ " ...)Step 4: API レスポンス自体を疑う
自分のコードではなく、受け取ったデータに問題があるケースも多いです。Unexpected token < in JSON at position 0 が出たなら、< は HTML の開始タグです。API エンドポイントが 404 や 500 の HTML エラーページを返しており、コードがそれを JSON として処理しようとしています。Content-Type: application/json ヘッダを確認し、レスポンスボディ全体をログに出力してください。
JSON フォーマッターツールは、上記の手順を GUI で自動化してくれます。貼り付けるだけでエラー行にジャンプし、Pretty Print で構造が見やすくなります。コマンドラインが使えない環境や素早く確認したい場面で活用してください。
よくある質問
JSON5 と JSONC はどう違うの?
どちらも JSON の厳格さを緩和した拡張形式ですが、目的が異なります。JSONC(JSON with Comments)はその名の通りコメントと末尾カンマのみを追加した最小拡張で、VS Code・TypeScript の tsconfig・ESLint などで採用されています。JSON5はより広範な拡張で、コメント・末尾カンマ・シングルクォート・16進数リテラル・省略されたキー(識別子形式)・複数行文字列などを許容します。どちらも標準の JSON.parse() では処理できず、専用パーサ(json5 npm パッケージ、VS Code 内蔵の JSONC パーサなど)が必要です。
trailing comma を許すツールはある?
はい、いくつかあります。JSON5 パーサは末尾カンマを許容します。jq は通常の JSON のみ受け付け末尾カンマはエラーになります。フォーマッターとして使う場合は hjson(Human JSON)というツールもあり、末尾カンマを自動除去しながら有効な JSON に変換できます。ただし、API 間通信など相互運用が必要な場面では、標準準拠の JSON を使うことを強く推奨します。末尾カンマを許すパーサを使うのは設定ファイルに限定してください。
パースエラー位置が「line 1 column 1」になるのはなぜ?
これは入力の先頭が完全に不正な文字で始まっているケースです。最も多いのは3パターンです。① BOM: UTF-8 BOM(EF BB BF)が先頭にあり、パーサがそれを認識できない(本記事の9番を参照)。② HTML レスポンス: API が JSON ではなく HTML エラーページを返しており、< が最初の文字になっている。③ 空文字列: レスポンスボディが空(Content-Length: 0)なのに JSON.parse("") を実行している。JSON.parse() は空文字列を SyntaxError として処理します(undefined を返しません)。
JavaScript の JSON.parse と eval の違いは?
eval(str) は渡された文字列を JavaScript コードとして実行するため、シングルクォート・コメント・末尾カンマ・関数リテラルなどを含む「JSON 風の文字列」も処理できます。しかし eval は 任意コード実行の脆弱性です。eval('alert(1)') が実行されてしまいます。外部から受け取ったデータを eval で処理することは絶対に避けてください。JSON.parse() はデータのみを受け付け、コードを実行しません。JSON を処理する正しい方法は常に JSON.parse()(または信頼できるパーサライブラリ)です。
BOM が本当に実害を起こすことってあるの?
はい、実際の障害事例があります。典型的なのは、Windows 端末で編集した設定ファイルを Linux の本番サーバーにデプロイしてアプリが起動しなくなるケースです。ローカルの Windows 環境では Node.js がなんとかパースできても(一部バージョンでは BOM を許容する実装がある)、本番の異なる Node.js バージョンや Python サービスで JSONDecodeError が出て初めて気づくパターンです。CI/CD パイプラインで python3 -m json.tool を構文チェックステップとして入れておくと、このクラスの問題を本番前に検出できます。
巨大な JSON はどう扱えばいい?
数 MB を超える JSON は JSON.parse() や json.loads() に一括で読み込むと、メモリを大量消費しパースに時間がかかります。解決策はストリーミングパーサです。Node.js では stream-json パッケージが軽量で使いやすく、ネストしたオブジェクトを逐次処理できます。Python では ijson パッケージが SAX スタイルのイベントドリブンなパースを提供します。コマンドラインなら jq はストリーミングモード(jq --stream)をサポートしています。また、1 行に 1 JSON オブジェクトを置く NDJSON(Newline-Delimited JSON)形式を採用することで、行単位の処理が可能になり、ストリーミング処理が大幅に簡単になります。