TL;DR: クイックフィックス
アプリをクラッシュさせずにオブジェクトをログに記録する必要がある場合は、カスタムのreplacer関数を使用してください。このスニペットはWeakSetを使って既に訪問済みのオブジェクトを追跡し、スキップします。
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) return;
seen.add(value);
}
return value;
};
};
const jsonString = JSON.stringify(yourObject, getCircularReplacer());
このエラーが発生する理由
TypeError: Converting circular structure to JSONは、JSON.stringify()が自分自身を参照するオブジェクトに遭遇したときに発生します。JSONは厳密にツリー構造です。循環参照を導入すると、標準アルゴリズムが終点まで走査できないサイクルを持つグラフが生成されます。
この単純な壊れたセットアップを見てみましょう:
const user = { name: 'Alice' };
user.self = user; // 無限ループを作成する
JSON.stringify(user); // TypeErrorをスロー
Node.jsで複雑なインスタンスを扱う際に頻繁に遭遇します。例えば、Expressのrequestオブジェクト(req)にはサーバーやソケットへの内部参照が数百個含まれています。同様に、SequelizeやMongooseのモデルは親のコネクションにリンクバックすることが多く、DOM要素は親にリンクし、その親がまた子にリンクします。
実践的な解決策
1. WeakSetのReplacer(ログ記録に最適)
これはオブジェクトをコンソールやログファイルに出力する最も効率的な方法です。WeakSetを使って参照を保存します。WeakSetはガベージコレクションを妨げないため、メモリ効率が高いです。replacerが同じオブジェクトを2回見つけると、2番目の参照を単純に破棄します。
function safeStringify(obj) {
const cache = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) return;
cache.add(value);
}
return value;
});
}
2. 'flatted'ライブラリを使う(データ永続化に最適)
実際にデータを保存して後で復元する必要がある場合もあります。循環部分を削除すると、そのデータは永久に失われます。flattedライブラリは、シリアライズ時に循環参照を一意の識別子に置き換え、パース時に復元することでこれを解決します。
gzip圧縮後は約1.1KBの小さなパッケージです。npmでインストールします:
npm install flatted
使い方はネイティブのJSONグローバルと同じです:
const { stringify, parse } = require('flatted');
const circularObj = { a: 1 };
circularObj.itself = circularObj;
const serialized = stringify(circularObj);
const deserialized = parse(serialized);
console.log(deserialized.itself === deserialized); // true
3. Node.jsのutil.inspect(デバッグに最適)
変数の中身を確認したいだけならJSON.stringifyに頼る必要はありません。Nodeのネイティブutil.inspectはまさにそのために作られています。循環を自動検出し、クラッシュする代わりに[Circular]としてマークします。
const util = require('util');
const complexObject = { /* ... */ };
// depth: null でネストされた全レベルを表示する
console.log(util.inspect(complexObject, { depth: null, colors: true }));
修正を確認する方法
修正をデプロイする前に、以下の確認を行ってください:
- エラーが出ないかテストする: 呼び出しを
try-catchでラップします。replacerが正しく機能していれば、catchブロックがトリガーされなくなるはずです。 - 出力を検査する:
WeakSetアプローチを使用した場合、欠けているキーが実際にスキップするつもりだったものであることを確認します。 - nullをチェックする: 参照を削除すると、特定のデータを期待するフロントエンドコンポーネントを壊す可能性のある
null値が残ることがあります。
予防とベストプラクティス
構造が見えると深いオブジェクトのデバッグが楽になります。ToolCraftのJSON Formatter & Validatorなどのツールを使えば、データの塊を貼り付けて子ノードが親に戻っている箇所を特定できます。ブラウザ内でローカルにデータを処理するため、設定ファイルはプライベートなまま保たれます。
このエラーを完全に避けるには:
- DTO(データ転送オブジェクト)を使う: 生のデータベースモデルをstringifyしないでください。代わりに、必要なフィールドのみ(例:
id、email、name)を含むプレーンオブジェクトにマッピングします。 - 状態をフラットに保つ: ReactやReduxでは、循環参照はアーキテクチャ上の問題を示す警告サインであることが多いです。フラットでシリアライズ可能な状態を目指してください。
- 手動でのプルーニング:
user.managerのような特定のキーがループを引き起こしていると分かっている場合は、stringifyを呼び出す前にそれを削除するかundefinedに設定します。

