validateList static method

BatchValidationResult validateList({
  1. required List raw,
  2. required Map<String, List<Type?>?> expectedTypes,
  3. String context = 'UnknownModel',
  4. Set<String> optional = const <String>{},
  5. bool strict = false,
  6. bool warnUnexpected = false,
  7. bool escalate = false,
  8. bool generatePreviews = true,
  9. Map<String, bool Function(Object?)>? validators,
})

Validates each element in raw against expectedTypes, treating non-Map items as failures.

Accepts a List<dynamic> as returned directly by jsonDecode, without requiring manual casting or filtering before calling validateBatch. Non-Map<String, dynamic> items at index N produce a JsonValidationResult.failure with the message "Item N is not a Map<String, dynamic>." and are counted in BatchValidationResult.failureCount and BatchValidationResult.failureIndices. Map items are validated against expectedTypes identically to validateBatch.

Use BatchValidationResult.failureIndices to skip invalid items when mapping raw to model instances:

// Full-or-nothing: return null if any item fails.
static List<UserModel>? tryFromListJson(List<dynamic> raw) {
  final BatchValidationResult batch = JsonSentinel.validateList(
    raw: raw,
    expectedTypes: {'id': [int], 'name': [String]},
    context: 'UserModel',
  );
  if (!batch.isValid) return null;
  return [for (final Object? item in raw) if (item is Map<String, dynamic>) UserModel.fromJson(item)];
}

// Skip-bad-items: return only the valid ones.
static List<UserModel> fromValidListJson(List<dynamic> raw) {
  final BatchValidationResult batch = JsonSentinel.validateList(
    raw: raw,
    expectedTypes: {'id': [int], 'name': [String]},
    context: 'UserModel',
  );
  return [
    for (int i = 0; i < raw.length; i++)
      if (!batch.failureIndices.contains(i) && raw[i] is Map<String, dynamic>)
        UserModel.fromJson(raw[i] as Map<String, dynamic>),
  ];
}

optional, strict, warnUnexpected, validators, escalate, and generatePreviews behave identically to validateBatch.

When generatePreviews is true, item_previews in the logger extras contains a JSON preview for Map items and the literal string '[non-Map item]' for non-Map items.

Implementation

static BatchValidationResult validateList({
  required List<dynamic> raw,
  required Map<String, List<Type?>?> expectedTypes,
  String context = 'UnknownModel',
  Set<String> optional = const <String>{},
  bool strict = false,
  bool warnUnexpected = false,
  bool escalate = false,
  bool generatePreviews = true,
  Map<String, bool Function(Object?)>? validators,
}) {
  final StackTrace stackTrace = StackTrace.current;
  assert(
    !(strict && warnUnexpected),
    'JsonSentinel.validateList: strict and warnUnexpected are mutually exclusive — '
    'use strict to fail on unexpected keys, or warnUnexpected to log without failing, not both.',
  );
  final List<JsonValidationResult> results = <JsonValidationResult>[];
  final List<List<String>> itemWarnings = <List<String>>[];
  for (int i = 0; i < raw.length; i++) {
    final Object? item = raw[i];
    if (item is Map<String, dynamic>) {
      final ({List<String> errors, List<String> warnings}) core = _validateCore(
        json: item,
        expectedTypes: expectedTypes,
        optional: optional,
        strict: strict,
        warnUnexpected: warnUnexpected,
        validators: validators,
      );
      itemWarnings.add(core.warnings);
      results.add(core.errors.isEmpty ? JsonValidationResult.success : JsonValidationResult.failure(core.errors));
    } else {
      itemWarnings.add(<String>[]);
      results.add(JsonValidationResult.failure(<String>['Item $i is not a Map<String, dynamic>.']));
    }
  }

  final BatchValidationResult batch = BatchValidationResult.fromResults(results);

  if (batch.failureCount == 0) {
    if (_verbose) {
      developer.log(
        '[$context] validateList() passed — ${raw.length} item(s) checked.',
        name: 'JsonSentinel',
      );
    }
  } else {
    final String itemsWord = raw.length == 1 ? 'item' : 'items';
    final StringBuffer buffer = StringBuffer(
      '[$context] JSON list validation failed (${batch.failureCount} of ${raw.length} $itemsWord failed):',
    );
    for (final int i in batch.failureIndices) {
      final List<String> errors = results[i].errors;
      if (errors.isEmpty) continue;
      final String errWord = errors.length == 1 ? 'error' : 'errors';
      buffer.write('\n  Item $i (${errors.length} $errWord):');
      for (final String e in errors) {
        buffer.write('\n    • $e');
      }
    }

    _log(
      buffer.toString(),
      stackTrace: stackTrace,
      extras: <String, Object?>{
        'context': context,
        'failure_count': batch.failureCount,
        'total_count': raw.length,
        if (generatePreviews)
          'item_previews': <String>[
            for (final int i in batch.failureIndices)
              if (raw[i] is Map<String, dynamic>) _jsonPreview(raw[i] as Map<String, dynamic>) else '[non-Map item]',
          ],
      },
      escalate: escalate,
    );

    if (_verbose) {
      developer.log(
        '[$context] validateList() failed — ${batch.failureCount} of ${raw.length} item(s) invalid.',
        name: 'JsonSentinel',
      );
    }
  }

  final List<int> warningIndices = <int>[
    for (int i = 0; i < itemWarnings.length; i++)
      if (itemWarnings[i].isNotEmpty) i,
  ];
  if (warningIndices.isNotEmpty) {
    final String itemsWord = raw.length == 1 ? 'item' : 'items';
    final StringBuffer wb = StringBuffer(
      '[$context] JSON list validation warning (${warningIndices.length} of ${raw.length} $itemsWord had unexpected keys):',
    );
    for (final int i in warningIndices) {
      final List<String> ws = itemWarnings[i];
      final String keyWord = ws.length == 1 ? 'key' : 'keys';
      wb.write('\n  Item $i (${ws.length} unexpected $keyWord):');
      for (final String w in ws) {
        wb.write('\n    • $w');
      }
    }
    _log(wb.toString(), stackTrace: stackTrace, extras: <String, Object?>{'context': context}, escalate: false);
    if (_verbose) {
      developer.log(
        '[$context] validateList() warned — ${warningIndices.length} of ${raw.length} item(s) had unexpected keys.',
        name: 'JsonSentinel',
      );
    }
  }

  return batch;
}