JavaScript中級 #7 JSONの扱いとシリアライズ
JavaScript中級シリーズの最後の記事です。データを外部とやり取りするときにほぼ必ず通る形式 — JSON を整理します。
JSON とは何ですか? #
JSON (JavaScript Object Notation) は JavaScript のオブジェクト構文からインスピレーションを得たデータ交換形式です。JavaScript だけでなく、ほぼすべての言語が標準でサポートしています。
{
"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 — 文字列 → オブジェクト
#
const text = '{"id":"u1","name":"カーティス"}';
const obj = JSON.parse(text);
obj.id; // 'u1'
obj.name; // 'カーティス'
文字列を JavaScript のオブジェクトに変換します。形式が間違っていると throw します。
JSON.parse('{name: "カーティス"}'); // ✗ SyntaxError (キーに引用符なし)
JSON.parse("{'a': 1}"); // ✗ SyntaxError (シングルクォート)
JSON.parse('{"a": 1,}'); // ✗ SyntaxError (末尾カンマ)
外部データを parse するときは、通常 try/catch で囲みます。
function safeParse(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}JSON.parse の reviver — 変換関数
#
第二引数として各キーと値を変換する関数を渡せます。
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 — オブジェクト → 文字列
#
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 がそれらに出会うと、静かに消えるか変形されます。
JSON.stringify({
a: undefined, // キー自体が消える
b: () => {}, // 関数も消える
c: Symbol('s'), // Symbol も消える
d: NaN, // null に変換
e: Infinity, // null に変換
f: -Infinity, // null に変換
});
// '{"d":null,"e":null,"f":null}'
a、b、c のキー自体が結果から抜け落ちます。これは時々事故を起こします — 「確かに値を入れたのに受け取る側に届かない」のよくある原因がこれです。
BigInt は throw #
JSON.stringify({ count: 100n }); // ✗ TypeError
BigInt は stringify が処理する方法を知らず、throw します。自分で変換する必要があります。
const obj = { count: 100n };
JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"count":"100"}'
toJSON メソッド — クラスが自分自身をシリアライズ
#
オブジェクトに toJSON メソッドがあれば、stringify はそのメソッドの戻り値を代わりにシリアライズします。
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 がまさにこのパターンを使っています。
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 で直接処理します。
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 が標準化されました。
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 に安全に保管するときに使います。
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 編で扱った内容:
- クラス —
#/static/get/set(#1) - 非同期 — Promise、async/await、Promise.all (#2)
- イテレータ / ジェネレータ —
Symbol.iterator、function*、yield(#3) - デストラクチャリング / spread / rest 深掘り — パラメータパターン、immutable update (#4)
?.と??— 安全アクセスと nullish のデフォルト値 (#5)- fetch API — 標準ネットワークツール、AbortController (#6)
- JSON の扱い — parse/stringify、structuredClone (この記事)
ここまで身につければモダン JavaScript の日常的な表現にはほぼ届きます。次の上級シリーズでは一段深く — クロージャ、this、プロトタイプ、イベントループ、メモリモデルまで JavaScript エンジンの動作原理を扱います。