自分自身を辿れるオブジェクトを JSON.stringify() に渡すと出ます。親子の相互参照、 DOM 要素、通信のオブジェクト。どこで輪になっているかの見つけ方と、3つの直し方を載せています。
自分自身に辿り着けるオブジェクト を文字列にしようとしたときに出ます。
javascript
const node = { name : 'a' }; node .self = node; JSON .stringify (node);
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'self' closes the circle
javascript
const parent = { name : '親' }; const child = { name : '子' , parent }; parent .child = child; JSON .stringify (parent);
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'child' -> object with constructor 'Object'
--- property 'parent' closes the circle
self を辿るとまた node に戻るので、終われません。
2行目から後ろは、どこで輪になったかを教えてくれる補足です。処理系によって出方が違います。
実務でいちばん多い形:親と子がお互いを持つ
example.js ▶ 実行↺ 戻す
const shared = { n: 1 };
console.log (JSON .stringify ({ a: shared, b: shared }));出力 書き換えて実行できます
{"a":{"n":1},"b":{"n":1}}
example.js ▶ 実行↺ 戻す
const parent = { name: '親' };
const child = { name: '子' , parent };
parent.child = child;
console.log (JSON .stringify (child, (key, value) => (key === 'parent' ? undefined : value)));出力 書き換えて実行できます
{"name":"子"}
parent → child → parent で輪になっています。
補足の行を上から読むと、どの鍵で戻ったかが分かります。
同じものが2回出るだけなら平気
example.js ▶ 実行↺ 戻す
const parent = { name: '親' };
const child = { name: '子' , parent };
parent.child = child;
console.log (JSON .stringify ({ name: child.name, parentName: child.parent.name }));出力 書き換えて実行できます
{"name":"子","parentName":"親"}
example.js ▶ 実行↺ 戻す
const node = { name: 'a' };
node.self = node;
const seen = new WeakSet ();
console.log (
JSON .stringify (node, (key, value) => {
if (typeof value === 'object' && value !== null ) {
if (seen.has (value)) return '[循環]' ;
seen.add (value);
}
return value;
}),
);出力 書き換えて実行できます
{"name":"a","self":"[循環]"}
同じものを2か所から指しているだけなら落ちません。
中身が2回書き出されるだけです。
問題になるのは自分に戻ってこられるとき だけです。
直し方1: 要らない側を落とす
いちばん簡単で、たいてい正しい方法です。
example.js ▶ 実行↺ 戻す
const node = { name: 'a' };
node.self = node;
const copied = structuredClone (node);
console.log (copied.name);
console.log (copied.self === copied);
console.log (copied === node);出力 書き換えて実行できます
a
true
false
example.js ▶ 実行↺ 戻す
class TreeNode {
constructor (name) {
this .name = name;
this .children = [];
}
add (child) {
this .children.push (child);
child.parent = this ;
return this ;
}
toJSON () {
return { name: this .name, children: this .children };
}
}
const root = new TreeNode ('根' );
root.add (new TreeNode ('子' ));
console.log (JSON .stringify (root));出力 書き換えて実行できます
{"name":"根","children":[{"name":"子","children":[]}]}
第2引数の関数(置換関数 )が、すべての鍵について呼ばれます。
undefined を返した鍵は出力から消えます。
送りたい形が決まっているなら、最初から作り直すほうが確実 です。
example.js ▶ 実行↺ 戻す
const rows = [
{ id: 1 , name: '根' , parentId: null },
{ id: 2 , name: '子' , parentId: 1 },
];
console.log (JSON .stringify (rows));出力 書き換えて実行できます
[{"id":1,"name":"根","parentId":null},{"id":2,"name":"子","parentId":1}]
javascript
const list = [ 1 ]; list .push (list); JSON .stringify (list);
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Array'
--- index 1 closes the circle
「全部送って、受け取る側で選ぶ」をやめると、この問題は起きません。
直し方2: 通ったものを覚えて切る
構造が分からないものを、とりあえずログに出したいときに使います。
javascript
console .log ( JSON .stringify ({ m : new Map ([[ 'a' , 1 ]]) , s : new Set ([ 1 ]) }));
{"m":{},"s":{}}
example.js ▶ 実行↺ 戻す
console.log (JSON .stringify ({ m: [...new Map ([['a' , 1 ]])], s: [...new Set ([1 ])] }));出力 書き換えて実行できます
{"m":[["a",1]],"s":[1]}
落ちなくなりますが、情報は欠けます。
記録や調査のためだけに使ってください。
WeakSet を使うと、覚えたオブジェクトが後片付けの邪魔になりません。同じ理由で Set より向いています。
直し方3: 複製したいだけなら structuredClone()
JSON を経由するのが目的でないなら、そもそも JSON.stringify() は要りません。
javascript
console .log ( JSON .stringify ( new Error ( '壊れた' )));
{}
structuredClone() は循環をそのまま保って 複製します。
別のオブジェクトになり、輪の形も残ります。
→ オブジェクトを結合する
直し方4: toJSON() を持たせる
そのオブジェクトを送るたびに同じ形にしたいなら、こちらです。
JSON.stringify() は、toJSON() があればその戻り値を使います。
parent は返していないので、輪になりません。
呼ぶ側は何も書かなくてよいので、忘れようがない のが利点です。
→ class — 作る・継承する・隠す
そもそも輪にしない
保存や送信をするデータなら、親を持たせずに id で指すほうが素直です。
表に入る形と同じです。受け取った側で木に組み直します。
循環は「画面で使うための形」であって、送るための形ではありません。
配列でも起きる
配列のときは、鍵の名前ではなく添字 が出ます。
ついでに:落ちないのに消えるもの
Map と Set は、落ちませんが中身が消えます。
配列にしてから渡してください。
→ Map と Set
どこで起きやすいか
出どころ
何が輪になるか
親子の木構造
子が親を持っている
DOM の要素
要素が親要素・文書・窓を辿れる
通信ライブラリの応答
応答が要求を持ち、要求が応答を持つ
エラーに付けた文脈
例外に付けたオブジェクトが例外を指す
ログを送ろうとして落ちる のが、いちばんよくある出会い方です。
エラーをそのまま JSON.stringify() に渡すのも、別の理由で失敗します。
→ Error と try / catch / finally / throw
探しかたの順序
文言の2行目から後ろを読む。 どの鍵で戻ったかが書いてある
その鍵が本当に要るか考える。たいてい要らない
要らないなら置換関数で落とすか、送る形を作り直す
中身が分からないものをとりあえず出したいなら、通ったものを覚えて切る
複製が目的なら structuredClone() に替える
このページの実行結果は Node 22.22.3 のものです。文言は処理系と版によって変わります。補足の行が出ない処理系もあります。
まとめ
自分に戻ってこられる形 だけが問題。同じものを2か所から指すのは平気
文言の2行目から後ろに、どの鍵で輪になったか が出る
直し方は要らない鍵を落とす ・通ったものを覚えて切る・toJSON() を持たせる・structuredClone()
Map と Set は落ちないが中身が消える
送る形を最初から作るのがいちばん確実
JSON.stringify(error) は落ちないが {} になる