JavaScriptにおいて、オブジェクトや配列などのデータ構造をJSON形式の文字列に変換する処理は、フロントエンドからバックエンドまで幅広いシーンで利用されています。
APIを通じたサーバーへのデータ送信や、ローカルストレージへの状態の保存など、その用途は多岐にわたります。
しかし、単にメソッドを呼び出すだけでは、特定のデータ型が消滅してしまったり、循環参照によってエラーが発生したりといった思わぬトラブルに遭遇することがあります。
この記事では、JSON.stringifyの基本的な使い方から、あまり知られていない第2・第3引数の活用法、そして実務で直面しやすいシリアライズの落とし穴までを詳しく解説します。
JSON.stringifyの基本操作
JavaScriptのオブジェクトをJSON文字列に変換するための最もシンプルな方法は、JSON.stringifyに変換したい対象を渡すことです。
このメソッドは、JavaScriptの値をJSONフォーマットに従った文字列へとシリアライズします。
まずは、基本的なオブジェクトを変換する例を見てみましょう。
// 基本的なオブジェクトの定義
const user = {
id: 1,
name: "田中太郎",
isAdmin: false
};
// JSON文字列に変換
const jsonString = JSON.stringify(user);
console.log(jsonString);
{"id":1,"name":"田中太郎","isAdmin":false}
このように、オブジェクトのプロパティ名と値がダブルクォートで囲まれた形式の文字列が得られます。
この基本操作は非常に簡単ですが、JSON.stringifyには、変換プロセスをカスタマイズするための強力な引数が用意されています。
replacer引数による詳細な制御
JSON.stringifyの第2引数は「replacer」と呼ばれ、シリアライズされる内容をフィルタリングしたり、値を加工したりするために使用されます。
replacerには、「配列」または「関数」を指定することができます。
配列によるプロパティの絞り込み
replacerに文字列の配列を渡すと、その配列に含まれるキー名を持つプロパティだけが抽出されてJSON化されます。
特定の機密情報を含めたくない場合や、必要なデータのみを転送したい場合に非常に便利です。
const employee = {
id: 101,
name: "佐藤次郎",
department: "開発部",
salary: 500000 // この情報は除外したい
};
// nameとdepartmentだけを抽出
const filteredJson = JSON.stringify(employee, ["name", "department"]);
console.log(filteredJson);
{"name":"佐藤次郎","department":"開発部"}
このように、指定しなかったプロパティであるidやsalaryは出力結果から完全に除外されます。
関数による値の動的な変換
replacerに関数を指定すると、各プロパティに対して変換処理を再帰的に適用することができます。
この関数は「キー」と「値」の2つの引数を受け取り、戻り値として返した内容がJSONに反映されます。
const settings = {
theme: "dark",
fontSize: 14,
apiKey: "secret_12345"
};
const replacerFunc = (key, value) => {
// 文字列型の値がある場合、すべて大文字に変換する
if (typeof value === "string" && key !== "apiKey") {
return value.toUpperCase();
}
// apiKeyプロパティの場合は値を隠匿する
if (key === "apiKey") {
return "********";
}
return value; // 変換しない場合はそのまま返す
};
const customJson = JSON.stringify(settings, replacerFunc);
console.log(customJson);
{"theme":"DARK","fontSize":14,"apiKey":"********"}
この関数形式のreplacerを利用することで、ロギング時に個人情報をマスクしたり、特定のデータ型を変換したりといった高度な制御が可能になります。
space引数で見やすいJSONを出力する
デフォルトのJSON.stringifyは、空白や改行を含まないコンパクトな文字列を出力します。
しかし、デバッグ目的などで人間が読みやすい形式(プリティプリント)で出力したい場合は、第3引数の「space」を利用します。
space引数には、インデントに使用するスペースの数、またはインデントとして使用する文字列を指定できます。
const data = {
status: "success",
items: [1, 2, 3],
meta: { total: 3 }
};
// インデントをスペース2つ分に設定
console.log(JSON.stringify(data, null, 2));
// インデントにタブ文字を使用する場合
console.log(JSON.stringify(data, null, "\t"));
{
"status": "success",
"items": [
1,
2,
3
],
"meta": {
"total": 3
}
}
実務では、ログファイルへの書き出しや設定ファイルの生成時に、この第3引数を活用して可読性を確保することが推奨されます。
なお、数値で指定する場合の最大値は10であり、それ以上の数値を指定しても10として扱われます。
オブジェクト独自の変換を定義するtoJSONメソッド
特定のオブジェクトがJSON.stringifyに渡されたとき、デフォルトの挙動ではなく独自のJSON表現を持たせたい場合があります。
そのような場合、オブジェクトにtoJSONという名前のメソッドを定義することで、シリアライズ時の出力をカスタマイズできます。
const product = {
id: 500,
name: "高性能ノートPC",
price: 150000,
// 独自のシリアライズロジック
toJSON() {
return {
productName: this.name,
formattedPrice: `¥${this.price.toLocaleString()}`
};
}
};
console.log(JSON.stringify(product));
{"productName":"高性能ノートPC","formattedPrice":"¥150,000"}
toJSONメソッドが存在する場合、JSON.stringifyはそのメソッドの戻り値をシリアライズの対象とします。
これは、クラス設計において、内部状態を隠蔽しつつ外部に公開するJSONの構造を固定したい場合に非常に有効なテクニックです。
シリアライズ時に注意すべき落とし穴
JSON.stringifyは万能ではなく、JavaScriptのすべてのデータ型をそのまま保持できるわけではありません。
ここでは、開発者が陥りやすい代表的な落とし穴とその回避策を解説します。
undefined、関数、シンボルの挙動
JSONは「データ交換フォーマット」であるため、JavaScript特有の概念であるundefined、関数、Symbolをそのまま表現することはできません。
これらがオブジェクトに含まれている場合、シリアライズの過程で自動的に削除されます。
const complexObj = {
valid: true,
undef: undefined,
func: () => { console.log("hello"); },
sym: Symbol("id")
};
console.log(JSON.stringify(complexObj));
{"valid":true}
一方、これらが配列の要素として含まれている場合は、インデックスを維持するためにnullへと変換されます。
const list = [1, undefined, () => {}, "test"];
console.log(JSON.stringify(list));
[1,null,null,"test"]
意図せずデータが消えてしまうのを防ぐには、事前にこれらの値を文字列に変換するか、適切なデフォルト値を設定しておく必要があります。
循環参照によるエラーの発生
オブジェクトが自分自身、あるいは親オブジェクトをプロパティとして参照している状態を「循環参照」と呼びます。
JSON.stringifyは再帰的にオブジェクトを走査するため、循環参照が含まれているとTypeErrorをスローして処理が中断されます。
const nodeA = { name: "A" };
const nodeB = { name: "B" };
nodeA.child = nodeB;
nodeB.parent = nodeA; // ここで循環参照が発生
try {
JSON.stringify(nodeA);
} catch (error) {
console.error("エラーが発生しました:", error.message);
}
エラーが発生しました: Converting circular structure to JSON
複雑なデータ構造を扱う場合は、前述のreplacer関数を利用して一度出現したオブジェクトを記録し、二度目以降は参照をスキップするような工夫が必要です。
BigIntのシリアライズ制限
JavaScriptの大きな整数を扱うBigInt型は、デフォルトではJSON.stringifyで変換することができません。
BigIntを変換しようとすると、循環参照と同様にTypeErrorが発生します。
これを解決するには、BigInt.prototype.toJSONを定義するか、replacer関数で数値や文字列に変換する必要があります。
const largeData = {
id: 1n // BigInt
};
// replacerでBigIntを文字列に変換する例
const safeJson = JSON.stringify(largeData, (key, value) =>
typeof value === "bigint" ? value.toString() : value
);
console.log(safeJson);
{"id":"1"}
MapやSet、Dateオブジェクトの扱い
MapやSetといった比較的新しいコレクション型も、JSON.stringifyでは空のオブジェクト{}として出力されてしまいます。
これらを正しくシリアライズするためには、配列に変換してから渡す必要があります。
一方で、Dateオブジェクトについては、標準でtoJSONメソッドが実装されているため、自動的にISO形式の文字列へと変換されます。
| データ型 | JSON.stringify実行後の結果 |
|---|---|
| Date | ISO 8601形式の文字列(例: “2026-05-15T…”) |
| Map / Set | 空のオブジェクト({})として出力される |
| NaN / Infinity | nullとして出力される |
| RegExp | 空のオブジェクト({})として出力される |
JSON.stringifyを用いたディープコピーとその代替手段
かつてJavaScriptでは、オブジェクトの深い階層までコピー(ディープコピー)するために、JSON.parse(JSON.stringify(obj))という手法が広く使われてきました。
しかし、これまで解説してきた通り、この手法には「関数やundefinedが消える」「Dateが文字列になる」といった重大な制約があります。
現代のJavaScript環境では、標準のAPIであるstructuredCloneを使用することが推奨されます。
const original = {
date: new Date(),
nested: {
val: 42
}
};
// JSONによるコピー(非推奨なケースが多い)
const jsonCopy = JSON.parse(JSON.stringify(original));
// structuredCloneによるコピー(推奨)
const modernCopy = structuredClone(original);
console.log(typeof jsonCopy.date); // "string" になってしまう
console.log(modernCopy.date instanceof Date); // true
structuredCloneは循環参照も適切にハンドルし、多くの組み込みオブジェクトを正しい型で維持したままコピーできます。
特別な理由がない限り、単なるコピー目的でJSON.stringifyを使用するのは避けましょう。
パフォーマンスとセキュリティの考慮事項
巨大なオブジェクトに対してJSON.stringifyを実行すると、メインスレッドを長時間ブロックする可能性があります。
ブラウザやNode.jsのメインスレッドが止まると、ユーザーインターフェースの応答性が低下したり、リクエスト処理が滞ったりします。
数メガバイトを超えるような巨大なデータを扱う場合は、処理を分割するか、ストリーム処理を検討してください。
また、セキュリティの観点では、JSON文字列をそのままHTMLに埋め込むことは避けるべきです。
ユーザーが入力したデータが含まれている場合、XSS(クロスサイトスクリプティング)の脆弱性を生む危険性があります。
HTML内にJSONを埋め込む必要がある際は、特定の文字(<, >, &など)をエスケープする処理を忘れないようにしましょう。
まとめ
JSON.stringifyは、JavaScript開発において避けては通れない非常に重要なメソッドです。
単なる文字列変換ツールとしてだけでなく、replacerによるフィルタリングやspaceによるフォーマット調整を活用することで、データの扱いやすさは劇的に向上します。
一方で、undefinedの消失やBigIntの制限、循環参照によるエラーなど、JavaScript特有の挙動を理解しておくことも欠かせません。
データのシリアライズが必要な場面では、本記事で紹介した特性や落とし穴を思い出し、安全かつ効率的なコードを記述することを心がけてください。
適切な手法を選択することで、バグの少ない堅牢なアプリケーション構築が可能になるはずです。
