removeCircular function
Removes circular references from an object graph for safe JSON serialization.
Returns a new data structure with circular references replaced by the
string "[Circular]". Does not mutate the original object.
Handles DateTime by converting to ISO 8601 UTC string and respects
objects with a toJson() method.
Implementation
Object? removeCircular(Object? obj, [Set<Object>? existingRefs]) {
if (obj == null || obj is bool || obj is num || obj is String) {
return obj;
}
if (obj is DateTime) {
return obj.toUtc().toIso8601String();
}
final refs = existingRefs ?? {};
// Handle objects with toJson() (custom serializable types).
if (obj is! Map && obj is! List) {
try {
final result = (obj as dynamic).toJson();
return removeCircular(result, refs);
} catch (_) {
return obj.toString();
}
}
if (refs.contains(obj)) {
return '[Circular]';
}
refs.add(obj);
late final Object? result;
if (obj is Map) {
final map = <String, Object?>{};
for (final MapEntry(:key, :value) in obj.entries) {
try {
if (value != null && refs.contains(value)) {
map[key.toString()] = '[Circular]';
} else {
map[key.toString()] = removeCircular(value, refs);
}
} catch (_) {
map[key.toString()] = '[Error - cannot serialize]';
}
}
result = map;
} else {
// obj is List
result = List<Object?>.generate((obj as List).length, (i) {
final value = obj[i];
try {
if (value != null && refs.contains(value)) {
return '[Circular]';
}
return removeCircular(value, refs);
} catch (_) {
return '[Error - cannot serialize]';
}
});
}
refs.remove(obj);
return result;
}