jsonSafe function
dynamic
jsonSafe(
- dynamic value, {
- JsonOptions options = const JsonOptions(),
- Object? toEncodable(
- 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);
}