JSONはオブジェクトではなく「テキスト」
JSON(JavaScript Object Notation)は、データをやり取りするためのテキスト形式です。見た目はJavaScriptのオブジェクトリテラルにそっくりですが、実体は文字列。つまり、ネットワーク越しに送ったり、ファイルに保存したり、設定ファイルに貼り付けたりできる、ただの文字の並びです。
// JavaScriptオブジェクト — メモリ上の生きた値。
const user = { name: "Rosa", age: 30 };
// JSON — 同じデータを表すテキスト文字列。
const json = '{"name":"Rosa","age":30}';
この2つは見た目が似ているので混同しがちです。頭の中で整理するコツは、オブジェクトはプログラムの中に存在するもの、JSONはその中身を外に送り出すときの姿と覚えること。この変換を担うのが JSON.stringify(オブジェクト → 文字列)と JSON.parse(文字列 → オブジェクト)の2つの関数です。
JSON.stringify の使い方:オブジェクトを文字列に変換する
任意のJavaScriptの値をJSON形式に変換するには、次のようにします。
出力されるのは、空白を含まない1行の文字列です。コンパクトなので、そのまま通信に乗せるのに向いています。typeof で確認してもわかるとおり、結果は object ではなく string です。
デバッグ中に見やすく整形したいときは、引数をあと2つ渡します。第2引数は replacer (後ほど解説します) で、null を渡せば「すべてのプロパティを出力する」という意味になります。第3引数はインデントの指定です。
これがいわゆる整形された(pretty-print)形式です。インデントは 2 または 4 スペースを使うのが定番で、ほとんどのツールの出力と揃います。
JSON.parse の使い方:文字列からオブジェクトへ
逆方向の処理です。JSON 文字列を渡すと、JavaScript の値として戻ってきます。
パースし終わればあとは普通のオブジェクトなので、ドット記法でもブラケット記法でも、配列メソッドでも何でも使えます。
JSON.parse はけっこう厳しくて、以下のようなケースではどれも SyntaxError が投げられます。
JSON.parse("{name: 'Rosa'}"); // unquoted key, single quotes
JSON.parse('{"name": "Rosa",}'); // trailing comma
JSON.parse("// a comment\n{}"); // comments aren't allowed
JSON.parse(""); // empty string
プログラムの「外側」から入ってくるデータ、たとえば fetch のレスポンスやファイル、ユーザー入力を扱うときは、必ず try/catch で囲んでパースしましょう。
ラウンドトリップで失われる値たち
JSONで扱える値は、文字列・数値・真偽値・null・配列・プレーンオブジェクトの6種類だけです。それ以外のJavaScriptの値は、変換時に消えたり、別物に化けたり、あるいはエラーの原因になります。
何が起きているのか整理すると、こんな感じです。
- 関数と
undefinedはオブジェクトから何も言わずに取り除かれます。配列の中ではnullに変換されます(JSON の配列は穴あきにできないため)。 DateオブジェクトはtoJSONメソッド経由で ISO 文字列にシリアライズされます。パースして戻ってくるのは文字列であって、Dateではありません。BigIntはTypeErrorを投げます。JSON の数値には対応する型がないからです。Map、Set、そして循環参照もそのままでは扱えません。
Date を行き来させたい場合は、JSON.parse の reviver 関数を使うのが定番の解決策です。
reviverはキーと値のペアごとに呼び出され、読み込み時に値を変換できます。
replacer:出力する項目を絞り込む
JSON.stringify の第2引数を使うと、出力する内容をコントロールできます。キーの配列を渡せば、ホワイトリストとして機能します。
関数を渡せば、任意のロジックも組めます。フィールドを削ったり、値をマスクしたり、その場で変換したりと自由自在です。
undefined を返すとそのキーは削除され、それ以外の値を返すとその値に置き換わります。
toJSON でシリアライズをカスタマイズする
オブジェクトに toJSON メソッドが定義されていると、JSON.stringify はそれを呼び出し、返り値のほうをシリアライズします。Date が独自のフォーマットで出力されるのはこの仕組みのおかげで、同じフックを自作のオブジェクトにも使えます。
クラスを自作する際、「誰がシリアライズしても同じ形で出力される」という一貫した公開フォーマットを持たせたい場面で非常に重宝します。
ディープコピー(昔ながらの裏技と、より良い方法)
長いこと、プレーンなオブジェクトをディープコピーする際のお決まりのワンライナーといえば JSON.parse(JSON.stringify(obj)) でした。
これで一応コピーはできます。ただし、中身が JSON で扱える値だけに限られます。Date や Map、関数が混ざっているとうまくいきません(上で触れたラウンドトリップの問題ですね)。
最近の JavaScript には structuredClone が用意されていて、こちらなら Date、Map、Set、型付き配列、さらには循環参照まできちんと扱えます。
できれば structuredClone を使うのが基本です。JSON.parse(JSON.stringify(...)) という小技は、プレーンなデータをサクッと複製したいときの「とっておき」として覚えておきましょう。
実践例:fetch で取得した JSON をパースする
JSON を一番よく触るのは HTTP 通信の場面です。fetch は自動で JSON をパースしてくれないので、レスポンスに対して .json() を呼び出します(中身は実質レスポンスボディに対する JSON.parse です):
JSONを送信する場合は逆の流れになります。ボディをJSON.stringifyで文字列化し、Content-Typeヘッダーを指定します。
await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Rosa", age: 30 }),
});
この2つのパターンを押さえておけば、実務で出会うJSON処理のほとんどはカバーできます。
次は: オプショナルチェーン
パースしたJSONには、存在するかどうか分からないフィールドがよく含まれています。たとえば user.address.city があったりなかったり、response.data.items が返ってこなかったり、といったケースです。こうした深くネストしたプロパティに、エラーを出さずに安全にアクセスするために使うのがオプショナルチェーン (?.) です。次のページで詳しく見ていきましょう。
よくある質問
JSONとJavaScriptオブジェクトは何が違うの?
JSONはあくまで「文字列(テキスト形式)」です。見た目はJavaScriptのオブジェクトリテラルに似ていますが、ルールはもっと厳しめ。キーは必ずダブルクォートで囲む、文字列もダブルクォート、値は文字列・数値・真偽値・null・配列・プレーンなオブジェクトに限定されます。一方でJavaScriptオブジェクトはメモリ上の生きた値で、関数やundefined、Dateインスタンスなど何でも持てます。
JavaScriptオブジェクトをJSONに変換するには?
JSON.stringify(obj)を呼ぶだけです。オブジェクトを辿ってJSON文字列を返してくれます。整形したいときは第3引数を使ってJSON.stringify(obj, null, 2)のように書けば、スペース2つでインデントされた見やすい形になります。ちなみに関数やundefinedは、オブジェクトのプロパティなら丸ごと消え、配列の要素ならnullに置き換わるので要注意。
JSON.parseでエラーが出るのはなぜ?
JSON.parseはかなり厳格で、末尾カンマ・シングルクォート・クォートなしのキー・コメントがあるだけでSyntaxErrorを投げます。ネットワーク経由のレスポンス、ファイル読み込み、ユーザー入力など、正しいJSONかどうか保証できない場面では必ずtry/catchで囲んでおきましょう。
JSON.stringifyでDateはそのまま保持される?
されません。Dateは"2026-01-15T10:30:00.000Z"のようなISO文字列に変換されます。JSON.parseで戻しても返ってくるのはただの文字列で、Dateオブジェクトにはなりません。必要ならJSON.parseのreviver引数を使って、ISO文字列をDateに復元する処理を書きましょう。