validateBatch static method

BatchValidationResult validateBatch({
  1. required List<Map<String, dynamic>> jsons,
  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 item in jsons against expectedTypes and returns a BatchValidationResult.

Runs all validations before emitting a single consolidated log entry, preventing one log event per failure when processing a list of API payloads. No log is emitted when all items pass, including the vacuously-valid case of an empty jsons list (which returns isValid: true with failureCount: 0 immediately).

Item indices in BatchValidationResult.failureIndices and in the log message are zero-based offsets into jsons.

optional, strict, warnUnexpected, validators, and escalate behave identically to validate. context identifies the model in log output. Defaults to 'UnknownModel'.

When warnUnexpected is true, a consolidated warning log is emitted after validation listing every item that contained unexpected keys. This is separate from the error log and does not affect BatchValidationResult.isValid.

When generatePreviews is true (the default), the logger receives an 'item_previews' key in extras containing a truncated JSON string for each failing item — equivalent to the 'json_preview' that validate attaches for single-item calls. Set generatePreviews to false to skip JSON serialisation for all failing items; useful for large high-failure batches where preview generation would be costly.

On failure the logger receives an extras map containing:

  • 'context' — the model name.
  • 'failure_count' — number of failing items (int).
  • 'total_count' — total items in the batch (int).
  • 'item_previews' — truncated JSON strings for each failing item, in failureIndices order (List<String>). Absent when generatePreviews is false.

Example log output when 2 of 5 items fail:

[UserRecord] JSON batch validation failed (2 of 5 items failed):
  Item 1 (1 error):
    • Missing required key 'name'.
  Item 4 (2 errors):
    • Key 'role' has invalid type. Expected: String; Actual: int.
    • Key 'active' cannot be null.

Implementation

static BatchValidationResult validateBatch({
  required List<Map<String, dynamic>> jsons,
  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.validateBatch: 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 (final Map<String, dynamic> json in jsons) {
    final ({List<String> errors, List<String> warnings}) core = _validateCore(
      json: json,
      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));
  }

  final BatchValidationResult batch = BatchValidationResult.fromResults(results);

  if (batch.failureCount == 0) {
    if (_verbose) {
      developer.log(
        '[$context] validateBatch() passed — ${jsons.length} item(s) checked.',
        name: 'JsonSentinel',
      );
    }
  } else {
    final String itemsWord = jsons.length == 1 ? 'item' : 'items';
    final StringBuffer buffer = StringBuffer(
      '[$context] JSON batch validation failed (${batch.failureCount} of ${jsons.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': jsons.length,
        if (generatePreviews)
          'item_previews': <String>[
            for (final int i in batch.failureIndices) _jsonPreview(jsons[i]),
          ],
      },
      escalate: escalate,
    );

    if (_verbose) {
      developer.log(
        '[$context] validateBatch() failed — ${batch.failureCount} of ${jsons.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 = jsons.length == 1 ? 'item' : 'items';
    final StringBuffer wb = StringBuffer(
      '[$context] JSON batch validation warning (${warningIndices.length} of ${jsons.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] validateBatch() warned — ${warningIndices.length} of ${jsons.length} item(s) had unexpected keys.',
        name: 'JsonSentinel',
      );
    }
  }

  return batch;
}