Circular references occur when an object references itself directly or indirectly, causing JSON.stringify() to throw an error.
Problem:
const obj = { name: 'John' };
obj.self = obj; // Circular reference
// JSON.stringify(obj); // Throws: TypeError: Converting circular structure to JSON
Solution 1: Custom replacer function:
function stringifyWithCircular(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;
});
}
const obj = { name: 'John' };
obj.self = obj;
console.log(stringifyWithCircular(obj));
// {"name":"John","self":"[Circular]"}
Solution 2: flatted library (preserves structure):
import { stringify, parse } from 'flatted';
const obj = { name: 'John' };
obj.self = obj;
const serialized = stringify(obj);
const deserialized = parse(serialized);
console.log(deserialized.self === deserialized); // true
Solution 3: Manual tracking with paths:
function safeStringify(obj, space) {
const seen = new Map();
let index = 0;
return JSON.stringify(obj, function(key, value) {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return `[Circular:${seen.get(value)}]`;
}
seen.set(value, index++);
}
return value;
}, space);
}
Solution 4: Remove circular references:
function removeCircular(obj) {
const seen = new WeakSet();
function detect(obj) {
if (typeof obj === 'object' && obj !== null) {
if (seen.has(obj)) {
return undefined;
}
seen.add(obj);
if (Array.isArray(obj)) {
return obj.map(detect).filter(x => x !== undefined);
}
const cleaned = {};
for (const [key, value] of Object.entries(obj)) {
const cleaned value = detect(value);
if (cleanedValue !== undefined) {
cleaned[key] = cleanedValue;
}
}
return cleaned;
}
return obj;
}
return detect(obj);
}
const obj = { name: 'John', child: { name: 'Jane' } };
obj.child.parent = obj;
const clean = removeCircular(obj);
console.log(JSON.stringify(clean));
Solution 5: Using toJSON method:
class Node {
constructor(name) {
this.name = name;
this.parent = null;
this.children = [];
}
toJSON() {
return {
name: this.name,
children: this.children,
// Exclude parent to avoid circular reference
};
}
}
const root = new Node('root');
const child = new Node('child');
root.children.push(child);
child.parent = root;
console.log(JSON.stringify(root)); // Works fine