jsonSafe function

dynamic jsonSafe(
  1. dynamic value, {
  2. JsonOptions options = const JsonOptions(),
  3. Object? toEncodable(
    1. dynamic object
    )?,
})

Returns a JSON-encodable form of value, honoring options.

Use toEncodable to transform application-specific objects before built-in conversions apply.

Implementation

dynamic jsonSafe(
  dynamic value, {
  JsonOptions options = const JsonOptions(),
  Object? Function(dynamic object)? toEncodable,
}) {
  final seen = options.detectCycles ? <int>{} : null;

  dynamic walk(dynamic v) {
    // Handle null, bool, and string early.
    if (v == null || v is bool || v is String) return v;

    // Numbers with special-casing for non-finite doubles.
    if (v is num) {
      if (v is double && (v.isNaN || v.isInfinite)) {
        switch (options.nonFiniteDoubles) {
          case NonFiniteDoubleStrategy.string:
            if (v.isNaN) return 'NaN';
            return v.isNegative ? '-Infinity' : 'Infinity';
          case NonFiniteDoubleStrategy.nullValue:
            return null;
          case NonFiniteDoubleStrategy.error:
            throw UnsupportedError('Non-finite double not allowed in JSON: $v');
        }
      }
      return v; // finite number
    }

    // Optional cycle detection.
    if (options.detectCycles) {
      final id = identityHashCode(v);
      if (seen!.contains(id)) return options.cyclePlaceholder;
      seen.add(id);
    }

    // Allow a caller-provided encoder to run first.
    if (toEncodable != null) {
      final transformed = toEncodable(v);
      if (transformed != null && !identical(transformed, v)) {
        final out = walk(transformed);
        if (options.detectCycles) seen!.remove(identityHashCode(v));
        return out;
      }
    }

    // Common helpful encodings.
    if (v is Enum) {
      final out = options.encodeEnumsAsName ? v.name : v.index;
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return out;
    }
    if (v is DateTime) {
      final out = switch (options.dateTimeStrategy) {
        DateTimeStrategy.iso8601String => v.toIso8601String(),
        DateTimeStrategy.millisecondsSinceEpoch => v.millisecondsSinceEpoch,
        DateTimeStrategy.microsecondsSinceEpoch => v.microsecondsSinceEpoch,
      };
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return out;
    }
    if (v is Duration) {
      final out = switch (options.durationStrategy) {
        DurationStrategy.milliseconds => v.inMilliseconds,
        DurationStrategy.microseconds => v.inMicroseconds,
        DurationStrategy.iso8601 => _durationToIso8601(v),
      };
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return out;
    }
    if (v is Uri) {
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return v.toString();
    }
    if (v is BigInt) {
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return v.toString();
    }
    if (v is Uint8List) {
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return base64Encode(v);
    }
    if (v is ByteBuffer) {
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return base64Encode(v.asUint8List());
    }
    if (v is ByteData) {
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return base64Encode(v.buffer.asUint8List());
    }

    // Collections.
    if (v is Map) {
      final map = <String, dynamic>{};
      v.forEach((key, val) {
        if (options.dropNulls && val == null) return;
        map[key.toString()] = walk(val);
      });
      final result = options.sortKeys
          ? SplayTreeMap<String, dynamic>.from(map, (a, b) => a.compareTo(b))
          : map;
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return result;
    }
    if (v is Set && options.setsAsLists) {
      final list = v.map(walk).toList();
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return list;
    }
    if (v is Iterable) {
      final list = v.map(walk).toList();
      if (options.detectCycles) seen!.remove(identityHashCode(v));
      return list;
    }

    // Fallback.
    if (options.detectCycles) seen!.remove(identityHashCode(v));
    if (options.stringifyUnknown) {
      return v.toString();
    }
    throw UnsupportedError(
      'Value of type ${v.runtimeType} is not JSON encodable. '
      'Provide JsonOptions.stringifyUnknown=true or a toEncodable handler.',
    );
  }

  return walk(value);
}