removeCircular function

Object? removeCircular(
  1. Object? obj, [
  2. Set<Object>? existingRefs
])

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;
}