coerceValue function
Repairs one value against its schema: losslessly, or not at all.
In order:
- Enum repair: a string differing from exactly ONE enum member only by case or padding becomes that member. Two members colliding case-insensitively make a third spelling ambiguous — untouched.
- Identity: a value already satisfying ANY declared type (
typemay be a union list) is never converted —"5"under["integer","string"]IS the string. The one exception is a whole double underinteger, folded to the canonical int. - Otherwise the declared types are tried in order and the first clean conversion wins; no clean conversion, no change.
- Structure recursion: maps repair their declared properties, lists repair
every element against
items— bounded by depth.
Idempotent: every repaired value satisfies its type, and step 2 makes satisfying values fixed points.
Implementation
Object? coerceValue(Object? value, Object? schema, [int depth = 0]) {
if (schema is! Map || depth > _maxDepth) return value;
var out = value;
final options = schema['enum'];
if (options is List && out is String && !options.contains(out)) {
final matches = options
.whereType<String>()
.where((e) => e.toLowerCase() == out.toString().trim().toLowerCase())
.toList();
if (matches.length == 1) out = matches.single;
}
final declared = schema['type'];
final kinds = switch (declared) {
final String s => [s],
final List<dynamic> l => l.whereType<String>().toList(),
_ => const <String>[],
};
if (!kinds.any((k) => _satisfies(out, k))) {
for (final kind in kinds) {
final converted = _coerceScalar(out, kind, kinds);
if (!identical(converted, out)) {
out = converted;
break;
}
}
} else if (kinds.contains('integer') &&
out is double &&
out.isFinite &&
out == out.roundToDouble()) {
out = out.toInt();
}
if (out is Map) {
final props = schema['properties'];
if (props is Map) {
out = {
for (final e in out.entries)
e.key: coerceValue(e.value, props[e.key], depth + 1),
};
}
} else if (out is List) {
final items = schema['items'];
if (items is Map) {
out = [for (final v in out) coerceValue(v, items, depth + 1)];
}
}
return out;
}