Geonode logo
Geonode Team

Geonode Team

更新日:2026年10月7日

公開日:2026年9月2日

JavaScriptのJSON.parse():完全ガイド

`JSON.parse()` 文字列をJavaScriptの値に変換します。これがAPIのすべてであり、習得するのに10秒ほどしかかかりません。 興味深いのは、その周辺にあるあらゆる要素です。つまり、リバイバー関数、本番環境に気付かれずに持ち込まれてしまう数値の精度損失、`__proto__`という特殊なケース、そしてついに元のソーステキストを確認できるようになった、最近の標準仕様の追加などです。 このガイドでは、単に正常な動作だけでなく、失敗ケースも含めてこれらすべてを網羅しています。

プロキシ会社がなぜ「JSON.parse」について記事を書いているのか、一点補足しておきます。私たちはGeonodeというプロキシ販売会社ですが、お客様から最も頻繁に報告されるSyntaxErrorは、JSONとは全く関係がありません。それは「Unexpected token '<'」というエラーで、これはレスポンスがJSONではなくHTMLであったことを意味し、APIがブロックページ、ログインリダイレクト、またはエラーページを返したことを示しています。 そこで、率直な注意点を述べておきます:取得したデータに対して「JSON.parse」というエラーが発生している場合は、他の設定を変更する前に、生のレスポンス本文をログに記録してください。 10回中9回は、パーサーは正常に動作しており、Webページが送信されたことを正確に報告しているのです。 プロキシを購入しても、ブロックされたという特定のケースでのみこの問題は解決されます。URLの誤り、トークンの有効期限切れ、あるいは遵守すべきレート制限については何の解決にもなりません。まずはレスポンスを出力してください。

その点はさておき、実際のAPIについて説明します。

基本と実際に遭遇するエラー

const data = JSON.parse('{"name": "Ada", "born": 1815}');
// { name: "Ada", born: 1815 }

パラメータは2つ:テキストと、オプションの復元関数です。これだけです。

入力がJSONの文法に違反している場合、SyntaxError

がスローされます。JSONの文法はJavaScriptのオブジェクトリテラル構文よりも厳格であり、その点がユーザーを惑わせることがあります。失敗の原因は、主に以下の4つのケースに集約されます。

一重引用符。 MDN では明確に「JSONの文字列は二重引用符(一重引用符ではない)で囲まなければならない」と規定されています。JavaScriptとしては有効ですが、JSONとしては無効です。

JSON.parse("{'name': 'Ada'}");  // SyntaxError
JSON.parse('{"name": "Ada"}');  // fine

末尾のコンマ。 現代のJavaScriptでは有効ですが、JSONでは無効です:

JSON.parse("[1, 2, 3, 4, ]");  // SyntaxError

引用符で囲まれていないキー。 {name: "Ada"}

これは有効なオブジェクトリテラルですが、JSONではありません。キーは引用符で囲まれた文字列でなければなりません。

レスポンスがJSONではありませんでした。 上記で説明したものです。Unexpected token '<'

は、ボディが <

で始まっていることを意味し、これはHTMLであることを示します。Unexpected end of JSON input

は通常、空のボディを意味します — 204、切り詰められたレスポンス、または await を忘れた fetch などです。

常にこれをラップし、実際に受け取った内容をログに出力してください:

function parseOrThrow(text, url) {
  try {
    return JSON.parse(text);
  } catch (err) {
    throw new Error(
      `Failed to parse JSON from ${url}: ${err.message}. ` +
      `First 200 chars: ${text.slice(0, 200)}`
    );
  }
}

重要なのは ``text.slice(0, 200)`

の部分です。単なる ``SyntaxError

` であれば、解析に失敗したことを示しています。最初の 200 文字にその理由が記載されており、通常はすぐに原因がわかります。

リバイバー関数とその用途

2番目の引数は、値が解析される際にその値を変換します:

const data = JSON.parse(text, (key, value) => {
  if (key === "created") return new Date(value);
  return value;
});

正確に知っておくべき3つの挙動があります。

深さ優先で実行されます。 ネストされたプロパティは親プロパティよりも先に処理され、最後の呼び出しではルート値のキーとして空の文字列が使用されます。 したがって、リバイバーがオブジェクトを処理する段階では、その子要素はすでに処理済みとなっています。

**undefined

を返すと、そのプロパティが削除されます。** MDN:「reviver

関数がundefined

を返す(または値を返さない)場合、そのプロパティはオブジェクトから削除されます。」 これはうっかり引き起こしやすい問題です。条件分岐の末尾に達したリバイバーは ``undefined`

を返し、キーを黙って削除してしまいます。デフォルトとして、常に明示的に ``value

` を返すようにしてください。

ルートは完全に置き換えられます。 「reviver

から別の値を返した場合、その値は最初に解析された値を完全に置き換えます。これはルート値にも適用されます。"

JSON には日付型がないため、典型的な用途は日付の復元です:

const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;

const data = JSON.parse(text, (key, value) =>
  typeof value === "string" && ISO_DATE.test(value)
    ? new Date(value)
    : value
);

パターンマッチングには注意が必要です。日付のような形式のものをすべて変換するリバイバーは、文字列として保持したい文字列(バージョン番号、識別子、たまたまタイムスタンプのように見えるユーザーコンテンツなど)も変換してしまいます。スキーマが分かっている場合は、キー名でのマッチングを優先してください。

また、コストにも注意が必要です。リバイバーはドキュメント内の値ごとに1回呼び出されます。大規模なペイロードの場合、これはパフォーマンス上の重大な考慮事項となります。多くの場合、単純にパースし、その後必要なフィールドのみを変換する方がコスト効率が良いでしょう。

ソーステキストへのアクセス:context.source および JSON.rawJSON

これは最近追加された機能の中で最も重要なものであり、多くの開発者はまだこれに触れたことがないでしょう。

JSON.parse ソーステキストへのアクセスに関する提案は、TC39プロセスのステージ4に達しました。これは、標準として承認されたことを意味します。この提案は、「ECMAScriptの値とJSONテキスト間の変換には情報損失が生じる」という、提案書で直接指摘されている問題を解決するものです。

reviver は、プリミティブ値に対して 3 番目の引数を受け取るようになりました。MDN は ``context.source`

を「この値を表す元の JSON 文字列」と説明していますが、提案書ではさらに正確に、これを「句読点を含み、先頭および末尾の無意味な空白を除いたソーステキスト」と定義しており、これに加え ``index

、``input

、``keys

` についても言及しています。

これがなぜ重要なのかは、次の例を見れば明らかです:

const text = '{"id": 9007199254740993}';

JSON.parse(text).id;
// 9007199254740992  — wrong, silently

JSON.parse(text, (key, value, context) =>
  key === "id" ? BigInt(context.source) : value
).id;
// 9007199254740993n  — correct

ソースへのアクセスがない場合、reviverが数値を認識する時点で、その数値はすでにJavaScriptのdouble型に変換されています。介入する間もなく、精度は失われてしまうのです。context.source

を使用すれば、元の数字を取得できます。

この提案では、JSON.rawJSON()

も追加されています。これにより、生のJSONテキストを指定でき、JSON.stringify

がそれを変更せずに出力できるようになります。これにより、JSONから読み込んだBigIntを、データが破損することなく書き戻すことが可能になり、ラウンドトリップが完結します。

JSON.stringify({ id: JSON.rawJSON("9007199254740993") });
// '{"id":9007199254740993}'

これを利用する前に、対象環境でのサポートを確認してください。しかし、これはもはや回避策ではなく、大数問題に対する正しい解決策となっています。

数値の精度こそが、リリース時に残ってしまうバグである

この問題は、黙って失敗し、症状が根本原因とはかけ離れた場所に現れるため、独立したセクションを設ける価値がある。

JSONの数値はJavaScriptの数値に変換されますが、これはIEEE 754準拠のdouble型です。Number.MAX_SAFE_INTEGER(9,007,199,254,740,991)を超える整数は、すべて正確に表現できるわけではありません。MDNはこれを率直に「この処理の過程で精度が失われる可能性がある」と述べています。

危険な点は、例外が投げられないことです。数値は返されますが、それは単に送信された数値そのものではないのです。

JSON.parse('{"id": 12345678901234567890}').id;
// 12345678901234567000

実務上で問題となる場面:

  • データベースの識別子。 64ビット整数の主キーは安全な範囲を超えています。2つの異なるレコードが、同じJavaScript数値として解析されてしまう可能性があります。
  • Snowflake スタイルの ID。 いくつかの大規模プラットフォームで使用されており、日常的に制限値を超えています。
  • 小単位の金額。 セントやサトシ単位の多額の金額。
  • ナノ秒単位のタイムスタンプ。 1970年以降のナノ秒単位のエポック値は、すでに安全な範囲外となっています。

優先順位の高い順に3つの対策:

文字列として要求する。 APIを管理できる場合は、大きな識別子を文字列としてシリアライズしてください。これは最もクリーンな解決策であり、どこでも機能し、コストもかかりません。 MDNもまさにこれを推奨しています:「精度を損なうことなく大きな数値を転送する1つの方法は、それらを文字列としてシリアライズし、BigIntとして復元することです。」

context.sourceをリバイバーと併用する。 上記と同様、プロデューサーを制御できず、かつ環境がこれをサポートしている場合。

BigInt に対応した JSON ライブラリを使用する。 古い環境の場合、これを処理できるパーサーがいくつかあります。依存関係が増え、パフォーマンスが多少低下します。

効果がない方法:パース後に数値が「正しく見えるか」を確認すること。その時点では情報は失われており、誤った値と正しい値を区別することはできません。

__proto__ とプロトタイプ汚染

MDN は、JSON と JavaScript の意味が異なる唯一のケースを次のように指摘しています。「JSON テキストが、同じ JavaScript 式とは異なる値を表す唯一のケースは、\"__proto__\"キーを扱う場合です。」

JavaScriptのオブジェクトリテラルでは、__proto__はプロトタイプを設定します。一方、JSON.parseでは、通常の自身のプロパティを作成します:

const fromLiteral = { __proto__: { admin: true } };
fromLiteral.admin;             // true — prototype was set

const fromJson = JSON.parse('{"__proto__": {"admin": true}}');
fromJson.admin;                // undefined — plain own property
Object.hasOwn(fromJson, "__proto__");  // true

したがって、JSON.parse自体はここでは安全です。これは意図された正しい動作です。

危険なのは、その後に何が起こるかです。プロトタイプ汚染の脆弱性は、ほとんどの場合、危険なキーをフィルタリングしないコードによって、解析されたデータが別のオブジェクトにマージされる際に発生します:

// Unsafe: a naive deep merge can walk into Object.prototype
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object") {
      merge(target[key] ?? (target[key] = {}), source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

__proto__ を含むペイロードを投入すれば、プログラム全体において Object.prototype を改変できてしまいます。防御策:

危険なキーを明示的にフィルタリングする — __proto__, constructor, prototype — 信頼できないデータに接触するあらゆるマージや代入において。

Object.create(null) を使用する — 信頼できないキーを保持するオブジェクトに対しては、汚染されるプロトタイプが存在しないようにするため。

Map を使用する。構造化されたオブジェクトではなく、純粋にキーバリューストアを構築する場合にこれを使用します。

スキーマに基づいて検証を行う。これは一般的な解決策であり、他の問題も検出できます。解析と検証は別々のステップであり、どちらも必要です。

JSON.parse 対 eval 対 Response.json()

eval は絶対に使用しないでください。 これは任意のコードを実行してしまう上、この用途では処理速度が遅く、JSON ではないデータも受け入れてしまいます。JSON を解析する際に eval が適切なツールとなるケースは一切ありません。

** Response.json()** は、fetch を使用する際に適しています。これはボディを読み込み、1 ステップで解析を行います:

const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

res.ok のチェックは、多くの人が省略しがちな部分ですが、これを省略することが、冒頭で述べた Unexpected token '<' エラーの直接的な原因となります。fetch は HTTP エラーステータスを理由に処理を中断しません。つまり、403 や 500 であっても通常通り処理が進み、その後 .json() がエラーページを解析しようとしてしまいます。まずステータスを確認し、徹底したい場合は content-type も確認してください:

const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
const type = res.headers.get("content-type") ?? "";
if (!type.includes("application/json")) {
  const body = await res.text();
  throw new Error(`Expected JSON, got ${type}: ${body.slice(0, 200)}`);
}
const data = await res.json();

なお、Response.json() はリバイバーを受け付けません。リバイバーが必要な場合は、res.text() の後に JSON.parse を実行してください。

この点についてもライブラリによって異なります。2xx以外のステータスコードに対して自動的に解析を行い、例外をスローするライブラリもあり、その場合はエラー処理の場所が変わってきます。axios vs fetch でその挙動を比較しました。

ページをフリーズさせずに大規模なJSONを解析する

JSON.parseは同期処理であり、ブロックされます。メインスレッドで大規模なドキュメントを解析すると、その処理中はインターフェースがフリーズしてしまいます。これは、動作のカクつき(jank)の一般的な原因であり、診断も容易です。

大まかな目安:1メガバイト未満であれば、特に気にする必要はありません。 1~10メガバイトの場合は、最も処理が遅いターゲット端末で測定してください。10メガバイトを超える場合は、別の方法を検討してください。

手間のかかる順に、選択肢を挙げます:

Web Workerに移行する。 最も単純で確実な解決策です。メインスレッド外で解析を行い、結果を戻します。 結果の転送には構造化クローン処理のコストがかかるため、ワーカーが後続の処理も行う場合に最も効果的です。

取得するデータ量を減らす。 ページネーション、フィールドの選択、より限定されたエンドポイントなど。ほとんどの場合、これが正しい解決策ですが、APIの管理者と協議が必要になるため、ほとんどの場合見過ごされてしまいます。

ストリーミングパーサーを使用する。 ツリー全体を構築するのではなく、値が到着するたびにそれを出力するライブラリが存在します。ドキュメントが本当に大きい場合や、ペイロードの一部のみが必要な場合には、この手法が有効です。

改行区切りのJSONを使用する。 大規模なコレクションの場合、1行に1つのJSONドキュメントとする方が、段階的な処理が格段に容易になります。各行が独立して解析され、ストリームが途中で途切れても完全なレコードが得られるからです。フォーマットを制御できる場合は、これがより優れた設計となることが多く、他のフォーマットとのトレードオフについては、JSONとCSVの比較で解説しています。

「JSON.parse」が不適切なツールとなる場合

入力がJSONでない場合。 JSON5、JSONC、およびコメントや末尾のコンマを含む設定ファイルには、それぞれ専用のパーサーが必要です。JSON.parseはこれらを正しく拒否します。正規表現を使ってコメントを削除しようとすべきではありません。そうすると、本来書くつもりもなかったようなパーサーを作ることになってしまいます。

単なる解析だけでなく、バリデーションが必要な場合。 解析が成功したからといって、構文が有効だったということしかわかりません。必須フィールドが存在するか、正しい型であるかについては何も示していません。まず解析を行い、その後バリデーションを行ってください。2番目のステップにはスキーマ検証ライブラリが適切なツールであり、try/catchは適していません。

データが損失なく往復処理されなければならない場合。 大きな整数、日付、undefined、関数、Map、Set、NaN、Infinity — これらはいずれもJSONではそのままの形で保持されません。損失のない往復処理が要件である場合は、context.source や JSON.rawJSON を意図的に使用するか、その目的のために設計されたフォーマットを使用してください。

レンダリングのたびにパースを行っている場合。 ホットパスで同じ文字列を繰り返しパースするのは、まったくの無駄です。一度パースしてキャッシュしましょう。

文字列が、検証していないフェッチから取得された場合。 冒頭で挙げたケースですが、これが最も一般的であるため改めて指摘します。フェッチしたデータに対して JSON.parse が例外をスローする場合は、バグはアップストリームにあります。ステータスコードを確認し、コンテンツタイプを確認し、ボディをログに記録してください。パーサーは真実を伝えています。

よくある質問

JSON.parse はどのような役割を果たしますか?

JSON形式の文字列を、JavaScriptの値(オブジェクト、配列、文字列、数値、ブール値、またはnull)に変換します。オプションで、解析時に各値を変換できるリバイバー関数を指定できます。入力が有効なJSONでない場合、SyntaxError 例外をスローします。

JSON.parse で「Unexpected token '<'」というエラーが出る理由は?

これは、文字列が < で始まっているためです。これは、JSON ではなく HTML を受け取ったことを意味します。通常、エラーページ、ログインリダイレクト、またはブロックページなどが該当します。パーサーの動作は正しく、問題はリクエスト側にあります。レスポンスボディの最初の 200 文字をログに出力すれば、通常は原因が明らかになります。

JavaScriptで大きな数値を含むJSONをパースするにはどうすればよいですか?

reviverのcontext.source引数を使用して、元の桁を読み取り、BigIntを構築してください。値がreviverに到達する頃には、すでに精度が失われているためです。さらに良い方法は、APIを制御できる場合は、大きな識別子を文字列としてシリアライズすることです。

JSON.parse の reviver 関数とは何ですか?

各キー・値ペアに対して、深さ優先で呼び出されるオプションの 2 番目の引数です。処理は空文字列のキーを持つルートで終了します。この関数が返す値が、そのキーの値として置き換えられます。undefinedを返すと、そのプロパティが削除されます。通常、この関数の役割は、文字列を Date などのより詳細な型に変換することです。

JSON.parseは安全ですか?

コードの実行という点では、はい。evalとは異なり、何も実行することはありません。また、__proto__も安全に処理し、プロトタイプを設定するのではなく、単純な独自のプロパティを作成します。リスクは、その後の処理にあります。信頼できない解析済みデータをフィルタリングせずに他のオブジェクトにマージすること(__proto__やconstructor)が、プロトタイプ汚染の原因となります。

JSON.parse と Response.json() の違いは何ですか?

Response.json() は、Fetch レスポンスのボディを読み取り、1 ステップでパースします。また、リバイバーを受け付けません。JSON.parse は、すでに持っている文字列に対して動作します。fetch は HTTP エラーを理由に処理を中断しない点に注意してください。そのため、.json() を呼び出す前に res.ok を確認してください。そうしないと、エラーページをパースしてしまうことになります。

JSON.parse はコメントや末尾のコンマを処理できますか?

いいえ。どちらも無効な JSON であり、SyntaxError が発生します。入力にこれらが含まれている場合は、JSON5 または JSONC であるため、それらを削除する正規表現ではなく、その形式に対応したパーサーが必要です。

JSON.parse はメインスレッドをブロックしますか?

はい、同期処理です。1メガバイト未満のドキュメントであれば問題ありませんが、大規模なペイロードの場合、目に見えるほどのフリーズを引き起こします。パース処理を Web Worker に移行するか、取得するデータ量を減らすか、ストリーミングパーサーを使用してください。

まとめ `

`JSON.parse`` は 2 つのパラメータを持つシグネチャを持ち、その背後には驚くほど深い意味が隠されています。特に留意すべき点は、エラーが派手には現れず、静かに失敗してしまう部分です。

数値の精度に関する問題が最も深刻です。大きな整数が静かに破損し、例外も発生せず、下流の処理で何かが壊れるまで、誤った値と正しい値を見分けることができません。解決策としては、ソース側で識別子を文字列エンコードするか、reviverのcontext.source引数を使用することです。この機能は現在ステージ4にあり、標準の一部となっています。

reviverは、特に日付の処理において、現在よりももっと活用されるべきです。ただし、デフォルトのパスでvalueを返すのを忘れると、プロパティが静かに削除されてしまう点には注意が必要です。また、プロトタイプの汚染はJSON.parseの問題では全くありません(この関数は__proto__を正しく処理します)が、結果をマージする側にとっては問題であり、影響が及ぶほど近い位置にあるため、重要な問題となります。

それ以外の問題はすべて、ある一つの習慣に帰着します。取得したデータの解析に失敗した場合は、コードを変更する前に生のボディをログに出してください。エラーメッセージにはほぼ必ず答えが含まれており、たいていの場合、そもそもJSONを受け取っていなかったという事実が判明します。