Javascript
JSON.stringify()で生じる “Converting circular structure to JSON” の循環参照エラーの原因と対策を調べてみた
2022年6月30日
JavascriptやTypescriptでネストの深いオブジェクトをログ出力する時なんかによく使う
JSON.stringify()
で、たまに出る
circular structure (循環参照)
のエラーについて調べてみた。
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'from' -> object with constructor 'Object'
--- property 'from' closes the circle
at JSON.stringify (<anonymous>)
at Object.<anonymous> (/Users/nishizumikeita/playground/typescript-playground/src/cyclic.js:13:18)
at Module._compile (node:internal/modules/cjs/loader:1092:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1121:10)
at Module.load (node:internal/modules/cjs/loader:972:32)
at Function.Module._load (node:internal/modules/cjs/loader:813:14)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
at node:internal/main/run_main_module:17:47
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'from' -> object with constructor 'Object'
--- property 'from' closes the circle
at JSON.stringify (<anonymous>)
循環参照エラーを回避するには
JSON.stringify() を行おうとしているオブジェクトが循環参照しているかどうかわからないとき、エラー回避のためにできることは少なくとも3つある。
JSON.stringify() の第2引数 (replacer) を使う
専用のライブラリを使う (たとえば
cycle.js
)
try – catch でエラーを拾う
今回はこのうちの1つ目、第2引数にreplacerを指定する方法について解説する。
JSON.stringify() の第2引数 (replacer) を使う
MDNの公式ドキュメントでも案内されている、replacerの役割を持つ関数を自分で実装する方法。
JSON.stringify()
の
replacer
引数を使用して循環参照を検索してフィルタリングする方法を示しています (これによりデータ損失が発生します)。
mdn web docs – TypeError: cyclic object value
console.log(chicken);
// => <ref *1> { name: 'にわとり', from: { name: 'ひよこ', from: { name: 'たまご', from: [Circular *1] } } }
にわとり > ひよこ > たまご > にわとり > (以下ループ)
の循環参照になる。そしてMDNのドキュメントにあるreplacerの使用例を噛み砕いてわかりやすくしたのがこちら。
const getCircularReplacer = () => {
let cnt = 0; // 回数カウント用
const seen = new WeakSet(); // 「すでに見た」セット。ここにオブジェクトを貯めていく
// --- ここから ---
const replacer = (key, value) => {
cnt++;
console.log(`-------${cnt}回目------`);
console.log('- key :', key,);
console.log('- value :', value);
console.log('- すでに見た: ', seen.has(value));
console.log(`-----------------\n\n`);
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return '循環参照が発生しています。'; // すでに見たオブジェクトが出てきたら値を置き換える
seen.add(value); // 「すでに見た」セットにオブジェクトを追加する
return value;
// --- ここまでがreplacerの定義 ---
return replacer
const replacer = getCircularReplacer();
console.log(JSON.stringify(chicken, replacer));
getCircularReplacerは、
replacer関数を返す関数
になっている。JSON.stringify()の第2引数に渡されたreplacerは以下の挙動をする。
初回は key = “”, value = 第1引数のオブジェクト全体が渡される
2回目以降はオブジェクトのkeyとvalueが順番に渡される
replacerで何かしらの
値をreturnすると、該当するkeyに対する値を置き換え
て出力する
valueがオブジェクトの場合、そのオブジェクトのkey, valueに対してまたreplacerが順番に実行される (
再帰的に呼び出される
)
これを実行すると以下のようになる。
-------1回目------
- key :
- value : <ref *1> {
name: 'にわとり',
from: { name: 'ひよこ', from: { name: 'たまご', from: [Circular *1] } }
- すでに見た: false
-----------------
-------2回目------
- key : name
- value : にわとり
- すでに見た: false
-----------------
-------3回目------
- key : from
- value : <ref *1> {
name: 'ひよこ',
from: { name: 'たまご', from: { name: 'にわとり', from: [Circular *1] } }
- すでに見た: false
-----------------
-------4回目------
- key : name
- value : ひよこ
- すでに見た: false
-----------------
-------5回目------
- key : from
- value : <ref *1> {
name: 'たまご',
from: { name: 'にわとり', from: { name: 'ひよこ', from: [Circular *1] } }
- すでに見た: false
-----------------
-------6回目------
- key : name
- value : たまご
- すでに見た: false
-----------------
-------7回目------
- key : from
- value : <ref *1> {
name: 'にわとり',
from: { name: 'ひよこ', from: { name: 'たまご', from: [Circular *1] } }
- すでに見た: true
-----------------
{"name":"にわとり","from":{"name":"ひよこ","from":{"name":"たまご","from":"循環参照が発生しています。"}}}
replacerが7回実行されているのがわかる。
replacerはvalueがオブジェクトのときだけ「すでに見た」セット(seen)にvalueを保存していく。valueのオブジェクトが「すでに見た」セットに入っている場合は、別の値(ここでは”循環参照が発生しています。” の文字列)に置き換えている。
結果、以下の挙動になる。
にわとりオブジェクトを「すでに見た」セットに保存
名前「にわとり」を処理(何もしない)
ひよこオブジェクトを「すでに見た」セットに保存
名前「ひよこ」を処理(何もしない)
たまごオブジェクトを「すでに見た」セットに保存
名前「たまご」を処理(何もしない)
にわとりオブジェクトを「すでに見た」セットに保存…しようとして、「
すでに見たやんそれ
」となって、値を置き換える
で、その結果無限ループが途中で止まって無事出力できる。
"name": "にわとり",
"from": {
"name": "ひよこ",
"from": {
"name": "たまご",
"from": "循環参照が発生しています。"
このエラーを調べたのは、
axios
を使ってHTTP通信する時にたまに
同様のエラー
を見かけることがあったのがきっかけだった。初心者にもわかりやすく噛み砕いている記事があまりなかったので、備忘録も兼ねてまとめてみた。
今回の記事作成にあたって、以下の記事がわかりやすく、参考にさせていただいた。
なるべく前提知識がなくても理解できるようにしたつもりですが、わかりづらいところがあったらコメントで教えていただけると助かります。
ABOUT ME
愛知県瀬戸市出身。
ジャズ喫茶アルバイト > クラゲ研究の大学院生 > 台湾留学 > 大手機械メーカー勤務を経て2021年に都内某企業にエンジニアとして転職。東京で一番好きな場所・神田神保町界隈で働ける喜びを噛み締めている。
主な使用技術はAWSサーバレスアーキテクチャ、Typescript。
趣味は誰もが知っている名曲をギター1本にアレンジすること。
愛知県瀬戸市出身。
ジャズ喫茶アルバイト > クラゲ研究の大学院生 > 台湾留学 > 大手機械メーカー勤務を経て2021年に都内某企業にエンジニアとして転職。東京で一番好きな場所・神田神保町界隈で働ける喜びを噛み締めている。
主な使用技術はAWSサーバレスアーキテクチャ、Typescript。
趣味は誰もが知っている名曲をギター1本にアレンジすること。