coerceValue function

Object? coerceValue(
  1. Object? value,
  2. Object? schema, [
  3. int depth = 0
])

Repairs one value against its schema: losslessly, or not at all.

In order:

  1. 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.
  2. Identity: a value already satisfying ANY declared type (type may be a union list) is never converted — "5" under ["integer","string"] IS the string. The one exception is a whole double under integer, folded to the canonical int.
  3. Otherwise the declared types are tried in order and the first clean conversion wins; no clean conversion, no change.
  4. 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;
}