JavaScript中級 #7 JSONの扱いとシリアライズ

読了 7分

JavaScript中級シリーズの最後の記事です。データを外部とやり取りするときにほぼ必ず通る形式 — JSON を整理します。

JSON とは何ですか? #

JSON (JavaScript Object Notation) は JavaScript のオブジェクト構文からインスピレーションを得たデータ交換形式です。JavaScript だけでなく、ほぼすべての言語が標準でサポートしています。

JSON 例
{
  "id": "u1",
  "name": "カーティス",
  "age": 30,
  "tags": ["dev", "blog"],
  "active": true,
  "spouse": null
}

JavaScript のオブジェクトと似ていますが、いくつかの違いがあります。

  • キーは必ずダブルクォートで囲まれた文字列
  • 値は string / number / boolean / null / array / object のみ
  • 関数、undefined、Date、Symbol、BigInt は許可されない
  • 末尾カンマ不可 — {"a": 1,} は invalid

JavaScript のすべての値が JSON で表現できるわけではない、というのがポイントです。

JSON.parse — 文字列 → オブジェクト #

parse 基本
const text = '{"id":"u1","name":"カーティス"}';
const obj = JSON.parse(text);

obj.id;     // 'u1'
obj.name;   // 'カーティス'

文字列を JavaScript のオブジェクトに変換します。形式が間違っていると throw します。

不正な JSON
JSON.parse('{name: "カーティス"}');   // ✗ SyntaxError (キーに引用符なし)
JSON.parse("{'a': 1}");           // ✗ SyntaxError (シングルクォート)
JSON.parse('{"a": 1,}');          // ✗ SyntaxError (末尾カンマ)

外部データを parse するときは、通常 try/catch で囲みます。

安全な parse
function safeParse(text) {
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

JSON.parse の reviver — 変換関数 #

第二引数として各キーと値を変換する関数を渡せます。

reviver — Date 復元
const text = '{"createdAt": "2026-05-04T10:00:00Z", "name": "カーティス"}';

const obj = JSON.parse(text, (key, value) => {
  if (key === 'createdAt' && typeof value === 'string') {
    return new Date(value);
  }
  return value;
});

obj.createdAt instanceof Date;   // true

JSON には Date 型がないので、通常 ISO 文字列で送ります。parse の時点で reviver を使って Date オブジェクトに復元するのがよくあるパターンです。大規模なアプリでは zod のようなスキーマライブラリが同じ仕事をより強力にこなします。

JSON.stringify — オブジェクト → 文字列 #

stringify 基本
const obj = { id: 'u1', name: 'カーティス' };
JSON.stringify(obj);
// '{"id":"u1","name":"カーティス"}'

見やすい出力 — インデント #

第三引数としてインデント幅を渡せます。デバッグやファイル保存時に便利です。

インデント
JSON.stringify(obj, null, 2);
// {
//   "id": "u1",
//   "name": "カーティス"
// }

null の位置は第二引数(replacer、後述)。使わないなら null

replacer — フィルタリング / 変換 #

第二引数として含めるキーの配列または変換関数を渡せます。

キーだけ選んでシリアライズ
const user = { id: 'u1', name: 'カーティス', password: 'secret', age: 30 };

JSON.stringify(user, ['id', 'name', 'age']);
// '{"id":"u1","name":"カーティス","age":30}'
// password 除外
関数で動的変換
JSON.stringify(user, (key, value) => {
  if (key === 'password') return undefined;   // 除外
  return value;
});

undefined を返すとそのキーは結果に含まれません。パスワードのような機密情報のマスキングに便利なパターンです。

消える値たち — stringify の落とし穴 #

JavaScript の一部の値は JSON で表現できません。stringify がそれらに出会うと、静かに消えるか変形されます。

stringify の落とし穴
JSON.stringify({
  a: undefined,         // キー自体が消える
  b: () => {},           // 関数も消える
  c: Symbol('s'),        // Symbol も消える
  d: NaN,                // null に変換
  e: Infinity,           // null に変換
  f: -Infinity,          // null に変換
});
// '{"d":null,"e":null,"f":null}'

abc のキー自体が結果から抜け落ちます。これは時々事故を起こします — 「確かに値を入れたのに受け取る側に届かない」のよくある原因がこれです。

BigInt は throw #

BigInt
JSON.stringify({ count: 100n });   // ✗ TypeError

BigInt は stringify が処理する方法を知らず、throw します。自分で変換する必要があります。

BigInt シリアライズ
const obj = { count: 100n };

JSON.stringify(obj, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"count":"100"}'

toJSON メソッド — クラスが自分自身をシリアライズ #

オブジェクトに toJSON メソッドがあれば、stringify はそのメソッドの戻り値を代わりにシリアライズします。

toJSON でカスタム
class User {
  constructor(name, password) {
    this.name = name;
    this._password = password;
  }
  toJSON() {
    return { name: this.name };   // password 除外
  }
}

const u = new User('カーティス', 'secret');
JSON.stringify(u);
// '{"name":"カーティス"}'

ビルトインの Date がまさにこのパターンを使っています。

Date の toJSON
const d = new Date('2026-05-04T10:00:00Z');
d.toJSON();              // '2026-05-04T10:00:00.000Z'
JSON.stringify({ d });   // '{"d":"2026-05-04T10:00:00.000Z"}'

Date が ISO 文字列に自動変換される理由がこれです。

循環参照 — stringify が throw #

循環参照の落とし穴
const a = { name: 'カーティス' };
a.self = a;

JSON.stringify(a);   // ✗ TypeError: Converting circular structure to JSON

オブジェクトが自分自身を指していると、stringify は無限ループに陥らないように throw します。大きなグラフ(親参照を持つツリーなど)をシリアライズするときによく出会います。

解決方法: 循環参照を切るか replacer で直接処理します。

WeakSet で訪問追跡
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  });
}

structuredClone — JSON トリックのモダンな代替 #

昔はディープコピーをするために、次のようなトリックをよく使っていました。

古いディープコピーのトリック — 限界あり
const copy = JSON.parse(JSON.stringify(original));

これは関数、undefined、Date、Map、Set を扱えませんでした。Date は ISO 文字列に変わり、関数 / undefined は消えてしまいます。

ES2022 から #4 で見た structuredClone が標準化されました。

structuredClone — ビルトイン
const original = {
  name: 'カーティス',
  createdAt: new Date(),
  tags: new Set(['a', 'b']),
};

const copy = structuredClone(original);
copy.createdAt instanceof Date;   // true
copy.tags instanceof Set;          // true

ほとんどのデータ構造を正確にコピーしてくれます。関数とクラスインスタンスの prototype は移せないという制限はありますが、一般的なオブジェクト / 配列 / Date / Map / Set はすべて OK。新しいコードでは structuredClone がディープコピーの答えです。

よく使うミニレシピ #

1) 安全なディープコピー + フォールバック #

安全コピー
function deepCopy(value) {
  if (typeof structuredClone === 'function') {
    return structuredClone(value);
  }
  return JSON.parse(JSON.stringify(value));
}

2) URL safe encode/decode #

btoa / atob は base64 エンコードのヘルパー。小さなオブジェクトを URL / storage に安全に保管するときに使います。

object → base64
const data = { id: 'u1', name: 'カーティス' };

const encoded = btoa(JSON.stringify(data));
// "eyJpZCI6InUxIiwibmFtZSI6IuOCq+ODvOODhuOCo+OCuSJ9"

const decoded = JSON.parse(atob(encoded));
// { id: 'u1', name: 'カーティス' }

3) JSON 検証後にパース #

大規模なアプリでは zod のようなスキーマライブラリが標準ですが、軽い場面では直接検査するほうが短く済みます。

簡単な検証
function parseUser(text) {
  const obj = JSON.parse(text);
  if (typeof obj?.id !== 'string') throw new Error('id 欠落');
  if (typeof obj?.name !== 'string') throw new Error('name 欠落');
  return obj;
}

TypeScript + React 実践 #6 では zod で同じことをより強力に解くパターンを扱いました。

まとめ #

この記事で整理した内容:

  • JSON は string / number / boolean / null / array / object のみ、ダブルクォートのキー
  • JSON.parse の reviver で Date のような型を復元
  • JSON.stringify の第三引数で見やすいインデント
  • replacer でキーのフィルタリング / 変換
  • undefined / 関数 / Symbol は stringify で消える
  • BigInt は throw — 直接処理
  • toJSON メソッドがあれば stringify がその結果を使う(Date の動作原理)
  • 循環参照は throw — WeakSet で追跡
  • structuredClone がディープコピーのモダンな答え

中級シリーズを終えて #

7 編で扱った内容:

  1. クラス#/static/get/set (#1)
  2. 非同期 — Promise、async/await、Promise.all (#2)
  3. イテレータ / ジェネレータSymbol.iteratorfunction*yield (#3)
  4. デストラクチャリング / spread / rest 深掘り — パラメータパターン、immutable update (#4)
  5. ?.?? — 安全アクセスと nullish のデフォルト値 (#5)
  6. fetch API — 標準ネットワークツール、AbortController (#6)
  7. JSON の扱い — parse/stringify、structuredClone (この記事)

ここまで身につければモダン JavaScript の日常的な表現にはほぼ届きます。次の上級シリーズでは一段深く — クロージャ、this、プロトタイプ、イベントループ、メモリモデルまで JavaScript エンジンの動作原理を扱います。

X