JSON.parse は「JSON 形式の文字列を JavaScript の値に変換する」関数、JSON.stringify は「JavaScript の値を JSON 形式の文字列に変換する」関数です。 API との通信、localStorage への保存、設定ファイルの読み書きなど、データを文字列として受け渡す場面で必ず登場します。

この記事では、2つの関数の使い方を Node.js 22.22.2 と Chromium 141 で実際に動かした結果つき でまとめました。特に「Unexpected token」「Unexpected end of JSON input」「"[object Object]" is not valid JSON」などの 頻出エラーを、再現コード・実際のメッセージ・修正コードの3点セットで一覧化 しています。エラー文で検索してたどり着いた方は「エラー一覧」の章からお読みください。

JSON.parse と JSON.stringify の早見表

やりたいこと 書き方 結果
文字列 → オブジェクト JSON.parse('{"id":1}') { id: 1 }
オブジェクト → 文字列 JSON.stringify({ id: 1 }) '{"id":1}'
見やすく整形する JSON.stringify(obj, null, 2) 2スペースのインデントつき文字列
出力するキーを絞る JSON.stringify(obj, ['id', 'name']) 指定したキーだけ出力
値を加工して出力 JSON.stringify(obj, (key, value) => ...) replacer の戻り値が出力される
読み込み時に値を加工 JSON.parse(text, (key, value) => ...) reviver の戻り値が結果になる
例外を出さずに読む try { JSON.parse(text) } catch { ... } 不正な文字列でも処理が続く
fetch の応答を読む await response.json() 本文を JSON として解釈した値
localStorage に保存 localStorage.setItem(k, JSON.stringify(v)) 文字列として保存される
深いコピーを作る structuredClone(obj) Date・Map・Set も保ったコピー
引数 JSON.parse(text, reviver) JSON.stringify(value, replacer, space)
第1引数 JSON 形式の文字列 変換したい値
第2引数 reviver(値を加工する関数・省略可) replacer(関数またはキーの配列・省略可)
第3引数 なし space(インデント。数値または文字列・省略可)
戻り値 オブジェクト・配列・文字列・数値・真偽値・null 文字列(変換できない値のときは undefined)
例外 SyntaxError(JSON として読めないとき) TypeError(循環参照・BigInt のとき)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JSON.parseとJSON.stringifyの使い方|エラー原因と整形」

JSON.parse と JSON.stringify の基本

いちばん小さな使用例

次のコードを basic.mjs として保存し、node basic.mjs で実行できます。ブラウザのコンソールに貼っても同じ結果です。

// オブジェクト → JSON 文字列
const user = { id: 1, name: 'Sato', tags: ['a', 'b'] };
const text = JSON.stringify(user);
console.log(text);
console.log(typeof text);

// JSON 文字列 → オブジェクト
const obj = JSON.parse(text);
console.log(obj.name);
console.log(obj.tags[1]);

実行結果(Node.js 22.22.2):

{"id":1,"name":"Sato","tags":["a","b"]}
string
Sato
b

JSON.stringify の結果は「文字列」です。 見た目はオブジェクトに似ていますが、text.name のようにプロパティを読むことはできません。プロパティを読みたいときは JSON.parse でオブジェクトに戻します。

JSON として書けるもの・書けないもの

JSON は JavaScript のオブジェクトリテラルに似ていますが、ルールがより厳密 です。JSON.parse のエラーの多くは、この違いから生まれます。

書き方 JavaScript のオブジェクト JSON
キーをダブルクォートで囲む {"a":1} 書ける 書ける(必須)
キーを囲まない {a:1} 書ける 書けない
シングルクォート {'a':1} 書ける 書けない
末尾カンマ {"a":1,} 書ける 書けない
コメント // memo 書ける 書けない
undefined 書ける 書けない
NaN・Infinity 書ける 書けない
先頭ゼロの数値 01 書けない(strict mode) 書けない
文字列の中の生の改行 書けない(テンプレートリテラルを除く) 書けない(\n と書く)
関数・Date・Map などのオブジェクト 書ける 書けない

JSON で使える値は 文字列・数値・true・false・null・配列・オブジェクトの7種類だけ です。また、JSON のトップレベルはオブジェクトでなくてもかまいません。

console.log(JSON.parse('1'), JSON.parse('"a"'), JSON.parse('null'),
  JSON.parse('true'), JSON.parse('[1,"a"]'), JSON.parse(' {"a":1} '));
1 a null true [ 1, 'a' ] { a: 1 }

前後の空白は無視されます。同じキーが2回出てきた場合は後の値が採用されます(JSON.parse('{"a":1,"a":2}') は { a: 2 } でした)。

JSON.parse・JSON.stringify のエラー一覧(再現コード・メッセージ・修正)

ここからがこの記事の中心です。メッセージはすべて Node.js 22.22.2 で実際に出力されたもの です(同じ V8 エンジンを使う Chromium 141 でも、確認した5種類のメッセージは同じ文面でした)。Firefox や Safari はエンジンが異なるためメッセージの文面が変わります。文面は違っても原因は同じなので、この表の「原因」から探してください。

エラーメッセージ早見表

実際のメッセージ(V8) 原因 修正
Unexpected token '<', "<!DOCTYPE "... is not valid JSON JSON ではなく HTML が返ってきた URL とステータスコードを確認する
Unexpected end of JSON input 空文字列を渡した 空のときは parse しない
"[object Object]" is not valid JSON オブジェクトをそのまま渡した・保存した parse をやめる/保存時に stringify する
"undefined" is not valid JSON undefined を渡した 値があるか先に確認する
Expected double-quoted property name in JSON at position 7 オブジェクトの末尾カンマ 末尾カンマを取る
Unexpected token ']', "[1,2,]" is not valid JSON 配列の末尾カンマ 末尾カンマを取る
Expected property name or '}' in JSON at position 1 シングルクォート・キーの囲み忘れ ダブルクォートにする
Unexpected non-whitespace character after JSON at position 8 JSON の後ろにコメントや余分な文字 余分な文字を取る
Unexpected token 'u', "{"a":undefined}" is not valid JSON 値に undefined と書いた null にするかキーごと省く
Expected ',' or '}' after property value in JSON at position 6 途中で切れている(閉じ括弧なし) 送信側・保存側で全文があるか確認
Unterminated string in JSON at position 11 文字列の途中で切れている 同上
Bad control character in string literal in JSON at position 7 文字列内に生の改行やタブ \n と書く/stringify で作る
Converting circular structure to JSON 循環参照(TypeError) 参照を外す/replacer で除く
Do not know how to serialize a BigInt BigInt を含む(TypeError) 文字列に変換する

以下、順番に再現コードと修正コードを示します。

Unexpected token '<' … is not valid JSON

JSON を期待していた場所に HTML が届いたときのエラー です。API の URL が違っていて 404 ページが返った、ログイン画面に転送された、サーバー側のエラーページが返った、といった場面で出ます。

// 再現
JSON.parse('<!DOCTYPE html><html></html>');
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

fetch でも同じです。ローカルに立てた検証用サーバー(404 で HTML を返す /html)に対して response.json() を呼ぶと、次の結果になりました。

const res = await fetch('http://127.0.0.1:7560/html');
console.log(res.status, res.ok);
await res.json();
404 false
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON

修正:json() を呼ぶ前に res.ok を確認します。 本文をいったん文字列で受け取ると、何が返ってきたかをメッセージに含められます。

const res = await fetch(url);
const text = await res.text();
if (!res.ok) {
  throw new Error(`HTTP ${res.status}: ${text.slice(0, 40)}`);
}
const data = JSON.parse(text);
Error: HTTP 404: <!DOCTYPE html><html><body>Not Found</bo

Unexpected token の後ろの文字は「JSON として読めなかった最初の文字」です。'<' なら HTML、'u' なら undefined、'N' なら NaN、'/' ならコメントを疑うと早く原因にたどり着けます。

Unexpected end of JSON input

空文字列を渡したときのエラー です。本文のない応答(204 No Content など)、空のファイル、空の入力欄で起きます。

// 再現
JSON.parse('');
SyntaxError: Unexpected end of JSON input

検証用サーバーの /empty(204 で本文なし)に response.json() を呼んだ結果も同じでした。

204 true
SyntaxError: Unexpected end of JSON input

修正:空のときは parse せず、既定値を返します。

const text = await res.text();
const data = text === '' ? null : JSON.parse(text);

なお、Node.js 22 では途中で切れた JSON は別のメッセージになりました。JSON.parse('{"a":1') は Expected ',' or '}' after property value in JSON at position 6 (line 1 column 7)、JSON.parse('{"name":"Sa') は Unterminated string in JSON at position 11 (line 1 column 12) です。position は 0 から数えた文字位置 で、line と column も表示されるため、長い JSON でも場所を特定できます。

"[object Object]" is not valid JSON

文字列ではなくオブジェクトを JSON.parse に渡したときのエラー です。JSON.parse は引数をまず文字列に変換するため、オブジェクトは "[object Object]" という文字列になり、それが JSON として読めずにエラーになります。

// 再現1:すでにオブジェクトになっている値を parse した
JSON.parse({ a: 1 });
SyntaxError: "[object Object]" is not valid JSON

よくあるのは次の2つです。

場面 なぜ起きるか 修正
await res.json() の結果をさらに JSON.parse した json() の時点でオブジェクトになっている JSON.parse を外す
localStorage にオブジェクトをそのまま保存した 保存時に "[object Object]" へ変換される 保存時に JSON.stringify する

localStorage の例を Chromium 141 で確認しました。

// 再現2:stringify せずに保存
localStorage.setItem('user', { name: 'Sato' });
console.log(localStorage.getItem('user'));
JSON.parse(localStorage.getItem('user'));
[object Object]
SyntaxError: "[object Object]" is not valid JSON
// 修正:保存時に stringify、読み出し時に parse
localStorage.setItem('user', JSON.stringify({ name: 'Sato', tags: ['a'] }));
console.log(localStorage.getItem('user'));
console.log(JSON.parse(localStorage.getItem('user')));
{"name":"Sato","tags":["a"]}
{ name: 'Sato', tags: [ 'a' ] }

同じ仕組みで、JSON.parse(undefined) は "undefined" is not valid JSON になります。一方 JSON.parse(null) は文字列 "null" として読まれ、エラーにならず null が返りました。localStorage.getItem は未保存のキーに対して null を返すため、JSON.parse(localStorage.getItem('nothing')) は null になります。

末尾カンマ・シングルクォート・コメント・undefined

手書きの JSON や、JavaScript のオブジェクトをそのまま貼り付けた文字列で起きるエラーです。

JSON.parse('{"a":1,}');          // オブジェクトの末尾カンマ
JSON.parse('[1,2,]');            // 配列の末尾カンマ
JSON.parse("{'a':1}");           // シングルクォート
JSON.parse('{a:1}');             // キーを囲んでいない
JSON.parse('{"a":1} // memo');   // 後ろにコメント
JSON.parse('// memo\n{"a":1}');  // 先頭にコメント
JSON.parse('{"a":undefined}');   // undefined
JSON.parse('{"a":NaN}');         // NaN
JSON.parse('{"a":01}');          // 先頭ゼロ
JSON.parse('{"a":"x\ny"}');      // 文字列内に生の改行

実行結果(上から順):

SyntaxError: Expected double-quoted property name in JSON at position 7 (line 1 column 8)
SyntaxError: Unexpected token ']', "[1,2,]" is not valid JSON
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
SyntaxError: Unexpected non-whitespace character after JSON at position 8 (line 1 column 9)
SyntaxError: Unexpected token '/', "// memo
{"a":1}" is not valid JSON
SyntaxError: Unexpected token 'u', "{"a":undefined}" is not valid JSON
SyntaxError: Unexpected token 'N', "{"a":NaN}" is not valid JSON
SyntaxError: Unexpected number in JSON at position 6 (line 1 column 7)
SyntaxError: Bad control character in string literal in JSON at position 7 (line 1 column 8)

修正の基本は「JSON は手で組み立てず、JSON.stringify で作る」 です。stringify が作る文字列は必ず正しい JSON になり、クォートや改行のエスケープも任せられます。

// 直す前:文字列連結で組み立てる(名前に " や改行が入ると壊れる)
const bad = '{"name":"' + name + '"}';

// 直した後:オブジェクトを作ってから stringify
const good = JSON.stringify({ name });
console.log(JSON.stringify('a"b\n'));
"a\"b\n"

設定ファイルにコメントや末尾カンマを書きたい場合は、JSONC や JSON5 といった別の形式と、それに対応したパーサーを使う方法があります。JSON.parse 自体はそれらを読めません。

もう1つ、ファイルの先頭に BOM(バイトオーダーマーク。見えない文字 U+FEFF)が付いていると Unexpected token になります。ファイルから読んだ文字列でこのエラーが出るときは、次のように先頭を取り除きます。

const clean = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
const data = JSON.parse(clean);

Converting circular structure to JSON(循環参照)

オブジェクトが自分自身を(直接または間接に)参照しているとき、JSON.stringify は TypeError を出します。 親が子を持ち、子が親を持つ構造が典型です。

// 再現
const parent = { name: 'parent' };
const child = { name: 'child', parent };
parent.children = [child];
JSON.stringify(parent);
TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'children' -> object with constructor 'Array'
    |     index 0 -> object with constructor 'Object'
    --- property 'parent' closes the circle

メッセージの最後の行 property 'parent' closes the circle が、輪を閉じているプロパティ名 です。ここを出力から外せば解決します。

// 修正1:replacer で輪を閉じるキーを除く
const text = JSON.stringify(parent, (key, value) =>
  key === 'parent' ? undefined : value
);
console.log(text);
{"name":"parent","children":[{"name":"child"}]}
// 修正2:どこに循環があるかわからないとき(WeakSet で一度見たオブジェクトを記録)
function stringifySafe(value, space) {
  const seen = new WeakSet();
  return JSON.stringify(value, (key, v) => {
    if (typeof v === 'object' && v !== null) {
      if (seen.has(v)) return '[Circular]';
      seen.add(v);
    }
    return v;
  }, space);
}

修正2は循環していなくても「同じオブジェクトが2か所から参照されている」場合に2回目を [Circular] に置き換える点に注意してください。ログ出力など、内容を目で確認する用途に向いています。DOM 要素やフレームワークのインスタンスは循環参照を含むことが多いため、必要なプロパティだけを取り出した新しいオブジェクトを stringify するのが確実です。

Do not know how to serialize a BigInt

BigInt(末尾に n が付く大きな整数)は JSON.stringify で変換できず、TypeError になります。

// 再現
JSON.stringify({ id: 9007199254740993n });
TypeError: Do not know how to serialize a BigInt
// 修正:replacer で文字列にする
const text = JSON.stringify({ id: 10n, n: 1 }, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
console.log(text);
{"id":"10","n":1}

逆に、読み込み側にも注意点があります。JSON.parse は数値を通常の Number として読むため、2^53 を超える整数は近い値に丸められます。

console.log(JSON.parse('{"id":9007199254740993}'));
{ id: 9007199254740992 }

末尾が 3 から 2 に変わりました。大きな ID は、送る側で文字列("id":"9007199254740993")にしてもらうのが最も確実 です。Node.js 22.22.2 では、reviver の第3引数 context.source(元の文字列表現)と JSON.rawJSON も動作しました。

// 読み込み:元の文字列から BigInt を作る
console.log(JSON.parse('{"id":9007199254740993}', (key, value, context) =>
  key === 'id' ? BigInt(context.source) : value
));

// 書き出し:数値のまま出力する
console.log(JSON.stringify({ id: JSON.rawJSON('9007199254740993') }));
{ id: 9007199254740993n }
{"id":9007199254740993}

これらは比較的新しい機能です。Node.js 22 以外の実行環境で使う場合は、MDN の対応表で確認してから採用してください。

JSON.stringify で整形する(space 引数)

第3引数 space に数値を渡すと、その数のスペースでインデントした読みやすい文字列になります。 第2引数を使わないときは null を渡します。

const o = { id: 1, name: 'Sato', tags: ['a', 'b'], address: { city: 'Tokyo' } };

console.log(JSON.stringify(o));           // 整形なし
console.log(JSON.stringify(o, null, 2));  // 2スペース
{"id":1,"name":"Sato","tags":["a","b"],"address":{"city":"Tokyo"}}
{
  "id": 1,
  "name": "Sato",
  "tags": [
    "a",
    "b"
  ],
  "address": {
    "city": "Tokyo"
  }
}
space の値 結果
省略・0・null 1行(改行なし)
2・4 などの数値 その数のスペースでインデント(上限は10)
'\t' タブでインデント
'--' などの文字列 その文字列でインデント(先頭10文字まで)

文字列 '--' を渡すと、次のようになりました。

{
--"id": 1,
--"name": "Sato",
--"tags": [
----"a",
----"b"
--],
--"address": {
----"city": "Tokyo"
--}
}

空の配列・空のオブジェクトは、整形しても []・{} のまま1行で出力されます。

すでにある JSON 文字列を整形する

1行にまとまった JSON 文字列を読みやすくしたいときは、parse してから stringify します。

const compact = '{"id":1,"items":[{"name":"pen","qty":2}]}';
const pretty = JSON.stringify(JSON.parse(compact), null, 2);
console.log(pretty);

画面に表示するときは <pre> 要素の textContent に入れると、改行とインデントがそのまま表示されます。デバッグ時に console.log(JSON.stringify(obj, null, 2)) と書けば、深い階層まで省略なく確認できます。

キーの順番

出力されるキーの順番は、オブジェクトのプロパティ順と同じです。整数のように見えるキーが先に昇順で並び、その後に文字列キーが追加順で並びます。

console.log(JSON.stringify({ b: 1, a: 2 }), JSON.stringify({ 2: 'x', 1: 'y', b: 1 }));
{"b":1,"a":2} {"1":"y","2":"x","b":1}

キーをアルファベット順にそろえたいとき は、第2引数にソート済みのキー配列を渡すか、先にキーを並べ替えたオブジェクトを作ります(配列を渡す方法は入れ子のキーにも同じ配列が適用されます)。

JSON.stringify で変わる値・消える値の一覧

JSON で表せない値を stringify に渡すと、エラーにはならず 別の値に変わるか、キーごと消えます。気づきにくいので、表で確認しておくと安心です。

値 オブジェクトのプロパティのとき 配列の要素のとき 単独で渡したとき
undefined キーごと消える null になる undefined(文字列ではない)
関数 キーごと消える null になる undefined
Symbol キーごと消える null になる undefined
NaN・Infinity null になる null になる 'null'
null null null 'null'
-0 0 0 '0'
Date ISO 形式の文字列 ISO 形式の文字列 ISO 形式の文字列
Map・Set {} になる {} になる '{}'
RegExp・Error {} になる {} になる '{}'
Uint8Array {"0":1,"1":2} の形 同左 同左
BigInt TypeError TypeError TypeError
クラスのインスタンス 自身の列挙可能プロパティのみ 同左 同左

実際に確認したコードと結果です。

const v = {
  u: undefined, f() {}, s: Symbol('x'), nan: NaN, inf: Infinity, nul: null,
  d: new Date('2026-10-02T03:04:05Z'), m: new Map([['a', 1]]), set: new Set([1, 2]),
  re: /ab/g, err: new Error('x'), arr: [undefined, () => 1, NaN], neg0: -0,
  str: 'あ', nested: { x: 1 }, ta: new Uint8Array([1, 2]),
};
console.log(JSON.stringify(v));
console.log(JSON.stringify(undefined), JSON.stringify(() => 1), JSON.stringify(NaN));
{"nan":null,"inf":null,"nul":null,"d":"2026-10-02T03:04:05.000Z","m":{},"set":{},"re":{},"err":{},"arr":[null,null,null],"neg0":0,"str":"あ","nested":{"x":1},"ta":{"0":1,"1":2}}
undefined undefined null

u・f・s の3つのキーが消え、Map と Set は中身が入っていても {} になっています。「送ったはずの項目が届いていない」ときは、値が undefined になっていないか を最初に確認すると解決が早くなります。値がないことを明示して送りたい場合は null を使います。

replacer で出力する項目を選ぶ・変換する

第2引数 replacer には 「キーの配列」または「関数」 を渡せます。

配列:出力するキーを絞る

const o = { id: 1, name: 'Sato', tags: ['a', 'b'], address: { city: 'Tokyo' } };
console.log(JSON.stringify(o, ['id', 'name']));
{"id":1,"name":"Sato"}

関数:値を加工する・取り除く

関数は (key, value) を受け取り、戻り値がそのキーの出力 になります。undefined を返すとそのキーは出力されません。

// パスワードを出力から除く
console.log(JSON.stringify(
  { id: 1, password: 'secret', name: 'Sato' },
  (key, value) => (key === 'password' ? undefined : value)
));

// Map と Set を出力できる形に変換する
console.log(JSON.stringify(
  { m: new Map([['a', 1]]), s: new Set([1, 2]) },
  (key, value) =>
    value instanceof Map ? Object.fromEntries(value)
    : value instanceof Set ? [...value]
    : value
));
{"id":1,"name":"Sato"}
{"m":{"a":1},"s":[1,2]}

replacer 関数は、最初に キーが空文字列 ''、値が全体 という形で1回呼ばれます。ここで undefined を返すと結果全体が undefined になるため、条件は「特定のキーのとき」「特定の型のとき」と限定して書くのがコツです。

replacer の種類 向いている用途 補足
キーの配列 出力したい項目が決まっている 入れ子のオブジェクトにも同じ配列が適用される
関数 型ごとの変換、項目の除外 入れ子のすべてのキーに対して呼ばれる

toJSON で「このオブジェクトの JSON 表現」を決める

オブジェクトに toJSON メソッドがあると、JSON.stringify はその戻り値を変換対象にします。 Date が ISO 形式の文字列になるのも、Date が toJSON を持っているからです。

class Price {
  constructor(amount, currency) { this.amount = amount; this.currency = currency; }
  toJSON() { return `${this.amount} ${this.currency}`; }
}
class User {
  constructor(name, password) { this.name = name; this.password = password; }
  toJSON() { return { name: this.name }; }   // password は出さない
}

console.log(JSON.stringify({ user: new User('Sato', 'secret'), price: new Price(1200, 'JPY') }));
console.log(new Date('2026-10-02T03:04:05Z').toJSON());
{"user":{"name":"Sato"},"price":"1200 JPY"}
2026-10-02T03:04:05.000Z
方法 決める場所 向いている場面
toJSON クラス(データ側) そのクラスをどこで stringify しても同じ形にしたい
replacer stringify を呼ぶ側 呼び出しごとに出力を変えたい、手を入れられないオブジェクトを扱う

両方ある場合は、toJSON の戻り値が replacer に渡されます。

Date は文字列になる → reviver で復元する

Date を stringify すると ISO 8601 形式(UTC)の文字列になり、parse しても Date には戻りません。 文字列のままです。

const text = JSON.stringify({ title: 'memo', createdAt: new Date('2026-10-02T03:04:05Z') });
console.log(text);

const p1 = JSON.parse(text);
console.log(typeof p1.createdAt, p1.createdAt);
{"title":"memo","createdAt":"2026-10-02T03:04:05.000Z"}
string 2026-10-02T03:04:05.000Z

このまま p1.createdAt.getFullYear() を呼ぶと、文字列にそのメソッドがないため TypeError になります。JSON.parse の第2引数 reviver を使うと、読み込みと同時に Date へ戻せます。

const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;

const p2 = JSON.parse(text, (key, value) =>
  typeof value === 'string' && iso.test(value) ? new Date(value) : value
);
console.log(p2.createdAt instanceof Date, p2.createdAt.getFullYear());
true 2026

形式で判定する方法は手軽ですが、たまたま同じ形の文字列も Date になります。項目が決まっているなら key === 'createdAt' のようにキー名で判定するほうが意図どおりに動きます。

reviver が呼ばれる順番

reviver は 内側(深い階層)から先に呼ばれ、最後にキー '' で全体が渡されます。

JSON.parse('{"a":1,"b":{"c":2}}', function (key, value) {
  console.log('key=' + JSON.stringify(key), 'value=' + JSON.stringify(value));
  return value;
});
key="a" value=1
key="c" value=2
key="b" value={"c":2}
key="" value={"a":1,"b":{"c":2}}

replacer と同じく、最後に必ず value を返す ことを忘れないようにします。何も返さないと undefined が返り、そのキーが結果から消えます。

安全な parse 関数(try/catch)

JSON.parse は不正な文字列に対して例外を投げます。ユーザーの入力、localStorage、外部からの応答など、中身を保証できない文字列を読むときは try/catch で囲みます。 毎回書くのは手間なので、関数にしておくと便利です。

/**
 * JSON として読めたら { ok: true, value }、読めなければ { ok: false, value: fallback, error } を返す
 */
function safeParse(text, fallback = null) {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (e) {
    return { ok: false, value: fallback, error: e.message };
  }
}

console.log(safeParse('{"a":1}'));
console.log(safeParse('{"a":1,}'));
console.log(safeParse('', []));
{ ok: true, value: { a: 1 } }
{
  ok: false,
  value: null,
  error: 'Expected double-quoted property name in JSON at position 7 (line 1 column 8)'
}
{ ok: false, value: [], error: 'Unexpected end of JSON input' }

戻り値を { ok, value } の形にしているのは、「読めなかった」と「読めた結果が null だった」を区別するため です。JSON.parse('null') は正しく null を返すので、読めなかったときに null を返すだけの関数では2つを見分けられません。

書き方 向いている場面
そのまま JSON.parse(text) 自分で stringify した直後など、形式が保証されている
safeParse(text, fallback) 入力欄・localStorage・外部の応答など、形式が保証されない
parse 後にスキーマ検証(Zod など) 形だけでなく、項目の有無や型まで確かめたい

JSON として正しくても、期待した項目がそろっているとは限りません。JSON.parse が保証するのは「JSON として読めた」ことまで で、data.user.name が存在するかどうかは別に確認します。TypeScript では JSON.parse の戻り値が any 型になるため、検証してから型を付ける流れにすると後の処理が書きやすくなります。

fetch の response.json() と JSON.parse の関係

response.json() は「本文を最後まで読み、JSON.parse した結果を返す」メソッド です。Promise を返すので await が必要です。fetch 全般の使い方は別記事「JavaScript fetchの使い方」で扱っているため、ここでは JSON に関わる点だけをまとめます。

const res = await fetch('http://127.0.0.1:7560/ok');
const data = await res.json();   // ここでオブジェクトになっている
console.log(data);
{ id: 1, name: 'Sato' }
つまずく点 実際の結果(Node.js 22) 対応
json() の結果をさらに JSON.parse "[object Object]" is not valid JSON JSON.parse を外す
404 などの HTML に json() Unexpected token '<', "<!DOCTYPE "... is not valid JSON res.ok を先に確認
本文なし(204)に json() Unexpected end of JSON input ステータスまたは本文の長さを確認
json() を2回呼ぶ TypeError: Body is unusable: Body has already been read 結果を変数に入れて使い回す
送信時に body へオブジェクトを直接渡す 本文が [object Object] になる JSON.stringify(data) を渡す

これらをまとめて扱う関数です。3つのエンドポイント(正常・404 の HTML・204 の本文なし)で動作を確認しました。

async function fetchJson(url, options) {
  const res = await fetch(url, options);
  const text = await res.text();
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${text.slice(0, 40)}`);
  if (text === '') return null;
  try {
    return JSON.parse(text);
  } catch (e) {
    throw new Error(`JSONではない応答です: ${text.slice(0, 40)}`, { cause: e });
  }
}
/ok { id: 1, name: 'Sato' }
/html HTTP 404: <!DOCTYPE html><html><body>Not Found</bo
/empty null

JSON を送るときは、body に stringify した文字列を渡し、Content-Type ヘッダーを付けます。

await fetchJson('/api/tasks', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: '資料を作る', done: false }),
});

localStorage に JSON で保存する

localStorage が保存できるのは文字列だけです。保存時に JSON.stringify、読み出し時に JSON.parse を対にして使います。Web Storage 全体の比較は別記事「localStorage・sessionStorage・Cookieの違い」にまとめているので、ここでは JSON の出し入れに絞ったコピペ用の関数を示します。

const storage = {
  save(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  },
  load(key, fallback = null) {
    const text = localStorage.getItem(key);
    if (text === null) return fallback;     // 未保存
    try {
      return JSON.parse(text);
    } catch {
      return fallback;                      // 壊れた値・以前の形式
    }
  },
};

storage.save('settings', { theme: 'dark', fontSize: 16 });
console.log(storage.load('settings'));
console.log(storage.load('nothing', { theme: 'light' }));
{ theme: 'dark', fontSize: 16 }
{ theme: 'light' }

上の出力は、localStorage と同じ動きをする簡易オブジェクトを用意して Node.js 22 で確認したものです。ブラウザでの保存と読み出しの挙動は、次の表のとおり Chromium 141 で確認しました。

確認したこと(Chromium 141) 結果
オブジェクトをそのまま setItem [object Object] という文字列が保存される
未保存のキーを getItem null
JSON.parse(null) エラーにならず null
stringify して保存 → parse 元のオブジェクトと同じ内容

読み出し側を try/catch で囲んでおくと、保存形式を途中で変えた場合や、手動で値を書き換えた場合でも画面が止まらず、既定値で動き続けます。Date を保存した場合は文字列で戻ってくるので、前の章の reviver を組み合わせます。

structuredClone と JSON の使い分け

JSON.parse(JSON.stringify(obj)) はオブジェクトの深いコピー(入れ子まで複製すること)の方法として広く使われてきました。現在は 深いコピー専用の structuredClone が使えます。2つの違いを Node.js 22.22.2 で比べました。

const src = {
  d: new Date('2026-10-02T03:04:05Z'), m: new Map([['a', 1]]), s: new Set([1]),
  u: undefined, n: NaN, re: /a/g, nested: { x: 1 },
};

console.log(structuredClone(src));
console.log(JSON.parse(JSON.stringify(src)));
{
  d: 2026-10-02T03:04:05.000Z,
  m: Map(1) { 'a' => 1 },
  s: Set(1) { 1 },
  u: undefined,
  n: NaN,
  re: /a/g,
  nested: { x: 1 }
}
{
  d: '2026-10-02T03:04:05.000Z',
  m: {},
  s: {},
  n: null,
  re: {},
  nested: { x: 1 }
}
値 structuredClone JSON.parse(JSON.stringify())
Date Date のまま 文字列になる
Map・Set 中身ごとコピー {} になる
undefined 保たれる キーが消える
NaN 保たれる null になる
BigInt 保たれる(10n を確認) TypeError
循環参照 コピーできる TypeError
関数 DataCloneError キーが消える
クラスのインスタンス プロパティのみ(クラスは外れる) プロパティのみ(クラスは外れる)

関数を含むオブジェクトを structuredClone に渡すと DataCloneError: f(){} could not be cloned. になりました。クラスのインスタンスは、どちらの方法でも instanceof が false になります。

使い分けの目安:コピーが目的なら structuredClone、文字列にして保存・送信するのが目的なら JSON です。JSON は「文字列にする」ことが本来の役割で、コピーはその副産物です。

JSON を HTML に埋め込むときの注意

JSON.stringify は < や > をエスケープしません。

console.log(JSON.stringify({ k: '</script>' }));
{"k":"</script>"}

サーバー側で <script> タグの中に JSON を直接書き出すと、値に </script> が含まれていた場合にそこでスクリプトが終わったと解釈されます。HTML に埋め込むときは < を別の表記に置き換えるか、<script type="application/json"> や data 属性に入れて textContent 経由で読む 方法が安心です。また、JSON 文字列を eval で評価する方法は、文字列の内容がそのままコードとして実行されるため、JSON.parse を使います。

よくある質問

Q. JSON.parse したのに、プロパティが文字列のままです。なぜですか? A. 二重に stringify されている可能性があります。JSON.stringify(JSON.stringify(obj)) の結果を1回 parse すると、オブジェクトではなく JSON 文字列が返ります。typeof で確認し、文字列であれば送信側・保存側で stringify が2回呼ばれていないかを見直してください。

Q. JSON.stringify の結果が {} になります。 A. Map・Set・Error など、列挙可能な自身のプロパティを持たないオブジェクトは {} になります。Map は Object.fromEntries(map)、Set は [...set]、Error は { name: e.name, message: e.message } のように、普通のオブジェクトや配列に変換してから stringify します。

Q. Unexpected token のエラーで、どこが原因かわかりません。 A. メッセージの Unexpected token の直後にある文字と、position・line・column を見ます。'<' なら HTML、'u' なら undefined、'/' ならコメントです。parse の直前に console.log(typeof text, text) を入れると、実際に渡している値を確認できます。

Q. JSON.parse は大きなデータでも使えますか? A. 使えます。ただし全体を一度に読み込んで処理するため、非常に大きな文字列では処理中に画面の操作が止まる時間が生じます。その場合は、データを分割して受け取る、Web Worker で parse する、といった方法を検討します(この記事では計測していません)。

まとめ

  • JSON.parse は文字列 → 値、JSON.stringify は値 → 文字列。stringify の結果は文字列なので、プロパティを読むには parse が必要です。
  • Unexpected token '<' は HTML が返っている、Unexpected end of JSON input は空文字列、"[object Object]" is not valid JSON はオブジェクトを渡している のが原因です。
  • JSON では末尾カンマ・シングルクォート・コメント・undefined・NaN が書けません。JSON は手で組み立てず stringify で作ると確実です。
  • 整形は JSON.stringify(obj, null, 2)。既存の JSON 文字列は parse してから stringify します。
  • undefined・関数はキーごと消え、NaN は null、Map・Set は {}、Date は文字列 になります。Date は reviver で復元できます。
  • 循環参照と BigInt は TypeError。replacer で除外・変換します。
  • 形式を保証できない文字列は try/catch つきの safeParse で読み、深いコピーには structuredClone を使います。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JSON.parseとJSON.stringifyの使い方|エラー原因と整形」

株式会社RJCでは、JSON の扱い方やエラーの読み解き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?