Unexpected end of JSON inputとは?意味と直し方

パーサーが完全な値を見つける前にテキストが尽きてしまった、という意味です。そしてほとんどの場合、原因はテキストがまったくなかったことです。

JSON.parse()やresponse.json()を呼ぶとSyntaxError: Unexpected end of JSON inputが発生する。Unexpected tokenとは違い、このエラーはおかしな文字を指しているわけではありません。パーサーが値を待っている途中で最後の文字にたどり着いた、という意味です。テキストが空だったか、ドキュメントの途中で途切れていたかのどちらかです。この2つを見分けることが作業のすべてであり、それにはログを1行追加するだけで足ります。

環境ごとのエラーメッセージ

表示される場所メッセージ
Chrome、Edge、Node.jsのJSON.parseUnexpected end of JSON input
ChromeとEdgeのresponse.json()同じメッセージ。先頭にFailed to execute 'json' on 'Response':が付くこともある
Chromeで、ボディがまったくないレスポンス(不透明なno-corsレスポンスなど)Unexpected end of input
FirefoxJSON.parse: unexpected end of data at line 1 column 1 of the JSON data
SafariJSON Parse error: Unexpected EOF
Pythonのjson.loadsExpecting value: line 1 column 1 (char 0)

メッセージにline 1、column 1、char 0が含まれていれば、パーサーは読むものを何も見つけられなかったということです。空のテキストは正しいJSONではありません。最も短い正しいドキュメントは、0、null、""、{}のような値です。

よくある原因:空のボディや空文字列

データの中のバグを探す前に、そもそもデータがあったかを確認してください。空文字列の出どころとしてよくあるのは次のものです。

  • 204 No Content。多くのAPIは、成功したDELETEやPUTにステータス204とボディなしで応答します。204でもres.okはtrueなので、これを確認しても、その後のres.json()呼び出しは守れません。
  • HEADリクエスト。HEADへのレスポンスには、ヘッダーに何が書かれていてもボディはありません。
  • mode: 'no-cors'を指定したfetch。これはCORSを回避する手段ではありません。ステータス0で、コードから読むことが許されないボディを持つ不透明なレスポンスが返るため、パースは必ず失敗します。サーバー側でCORSを有効にするか、自前のバックエンド経由でAPIを呼んでください。
  • 中身のない200や500。出力を書き込まないハンドラーや、何か送信する前にリクエストを終わらせてしまうサーバーエラーは、通常のステータスコードで空のボディを返します。
  • 空のストレージ値やファイル。localStorage.setItem('cart', '')、空のsettings.json、設定されているが中身が空の環境変数は、どれもJSON.parseに空文字列を渡すことになります。
  • 行区切りJSONの末尾の改行。text.split('\n').map(JSON.parse)は、最後の改行の後ろにある空文字列で失敗します。先に空行を取り除いてください。

よく似た2つのケースは挙動が異なります。localStorageに存在しないキーはnullを返し、JSON.parse(null)は例外を投げずにnullを返します。未設定の変数はundefinedで、こちらは代わりに"undefined" is not valid JSONで失敗します。これについてはUnexpected token in JSONで解説しています。

どちらのケースなのかを知るには、パースする前にボディをテキストとして読みます。

const res = await fetch('/api/cart', { method: 'DELETE' });
const text = await res.text();
console.log(res.status, res.type, res.headers.get('content-type'), text.length);
// Typical results:
// 204 basic null 0  -> nothing to parse
// 0 opaque null 0   -> a no-cors request
// 200 basic application/json 8192 -> there is text: check whether it is complete

ブラウザーでは、開発者ツールのNetworkパネルでも同じことが分かります。Responseタブが空なら、問題はパース処理のコードではなく、リクエストかサーバーにあります。

テキストが途中で切れていた場合

テキストが空でないなら、ドキュメントが完成する前に終わっています。現在のChromeとNode.jsが「Unexpected end of JSON input」と表示するのは、値が始まるはずの位置でテキストが止まったとき、またはtrue、false、nullの途中で止まったときだけです。それ以外の場所では、同じ途切れ方でも別のメッセージになります。

テキストの終わり方現在のChromeとNode.jsのメッセージ
{"id":Unexpected end of JSON input
{"tags":["a",Unexpected end of JSON input
{"ok":truUnexpected end of JSON input
{"name":"Al(11文字)Unterminated string in JSON at position 11
{"id":42(8文字)Expected ',' or '}' after property value in JSON at position 8

覚えておくと便利な目安があります。メッセージ中の位置がテキストの長さと等しければ、ドキュメントは形式が崩れているのではなく途中で切れています。古いバージョンのChromeとNode.jsはこうしたケースの大半も「Unexpected end of JSON input」と報告していたため、古いログや回答では途切れ全般を指してこの文言が使われています。

途中で切れたJSONは、たいてい次のいずれかから生まれます。

  • ストリームをチャンクごとにパースしている。res.body.getReader()、Node.jsのdataイベント、ソケットからのデータは、任意の大きさの断片で届きます。1つの断片が完全なドキュメントであることはまれです。チャンクを集めて最後に1回だけパースするか、行区切りJSONなら改行が来るまでバッファーに溜めてください。
  • 保存先が小さすぎる。書き込みを拒否せずに切り詰めるよう設定されたデータベースで、カラムの長さがドキュメントより短いと、先頭部分だけが保存されて残りは捨てられます。
  • 長いテキストを短くしてしまう場所からコピーしている。1行の最大長が決まっているログシステムなどです。
  • 接続が早く閉じた。Content-Lengthがなく、チャンク転送でもないレスポンスで起こります。長さが宣言されている場合は、接続が切れるとネットワークエラーとして報告されます。Node.jsではres.json()がTypeError: terminatedでrejectされます。

コード側で防ぐ

ボディをテキストとして読み、そのエンドポイントで空のボディが何を意味するかを決めてから、パースします。こうすれば204、HEAD、空の200を1か所で処理でき、ほかの問題が起きたときもエラーメッセージが役に立つまま保たれます。

async function readJson(res) {
  const text = await res.text();
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}: ${text.slice(0, 100) || '(empty body)'}`);
  }
  if (text.trim() === '') return null;   // 204, HEAD or an empty 200
  try {
    return JSON.parse(text);
  } catch (err) {
    throw new Error(`Invalid JSON (${text.length} chars): ${err.message}`);
  }
}

保存された値については、空と未設定を同じように扱い、既定値にフォールバックします。

function loadCart() {
  const raw = localStorage.getItem('cart');
  if (!raw) return [];          // null (missing) or '' (empty)
  try {
    return JSON.parse(raw);
  } catch {
    return [];                   // damaged value: start again
  }
}

サーバー側では一貫性を持たせてください。ボディなし、JSONのContent-Typeもなしで204を返すか、{}や{"deleted": true}のような小さなJSONドキュメントを返します。application/jsonと表示された空ボディの200レスポンスこそ、クライアントを壊す組み合わせです。

テキストを手作業で確認する

ボディが空でないときは、NetworkパネルやログからコピーしてJSON検証ツールに貼り付けてください。ブラウザー自身のパーサーを使うため、コードが受け取ったのと同じメッセージと位置が表示され、正しいJSONになった時点でドキュメントが整形されます。報告された位置が貼り付けたテキストのちょうど末尾なら、欠けた部分はデータではなく転送経路や保存先で探してください。HTMLページ由来の<のように特定の文字を示すエラーについては、Unexpected token in JSONを参照してください。

よくある質問

「Unexpected end of JSON input」とはどういう意味ですか?

パーサーがまだ値を待っている状態でテキストの終わりに達した、という意味です。多くはテキストが空だった場合で、たとえば204レスポンスや空の保存値です。ドキュメントが途中で切れていることもあります。

リクエストは成功したのに、なぜresponse.json()が失敗するのですか?

成功はボディがあることを意味しません。204 No Contentのレスポンスはokがtrueで、ボディはまったくありません。res.text()でボディを読み、空でないときだけパースしてください。

JSON.parse(null)でもこのエラーになりますか?

なりません。nullはテキスト「null」に変換され、これは正しいJSONなので、JSON.parse(null)はnullを返します。空文字列は「Unexpected end of JSON input」を投げ、undefinedは別のエラーを投げます。

空のボディと途中で切れたボディはどう見分けますか?

パースする前にテキストの長さをログに出します。0なら空です。長さが0より大きく、エラーの位置が長さと等しければ、ドキュメントは途中で切れています。