validate static method

JsonValidationResult validate({
  1. required Map<String, dynamic> json,
  2. required Map<String, List<Type?>?> expectedTypes,
  3. Set<String> optional = const <String>{},
  4. bool strict = false,
  5. bool warnUnexpected = false,
  6. bool escalate = false,
  7. String context = 'UnknownModel',
  8. String? parentContext,
  9. Map<String, bool Function(Object?)>? validators,
})

Validates json against expectedTypes and returns a JsonValidationResult.

All failures are collected before logging — a single log entry is emitted per call with every problem listed as a bullet. Returns JsonValidationResult.success when all validations pass.

expectedTypes defines the schema. Keys are the JSON field names; values are lists of allowed types. Including null in the list marks the field nullable. A null or empty list checks key existence only.

optional lists keys that are skipped when absent from json but still type-checked when present. Defaults to {}.

When strict is true, keys present in json but absent from expectedTypes are reported as errors. Defaults to false. Mutually exclusive with warnUnexpected.

When warnUnexpected is true, keys present in json but absent from expectedTypes are logged as warnings without failing validation — JsonValidationResult.isValid remains true. Mutually exclusive with strict; asserted in debug mode.

validators is an optional map of per-key predicate functions run after type validation passes for that key. Each predicate receives the field value as Object? and must return false to fail. A failing predicate adds a bullet error for the key. Predicates are skipped for absent optional keys and for keys that already failed type validation.

escalate is forwarded to the logger as a hint that this failure warrants elevated capture (e.g. a Sentry event rather than a breadcrumb). Defaults to false. Warning logs always use escalate: false.

context identifies the model in log output. Defaults to 'UnknownModel'.

parentContext is an optional parent path prepended to context in log output and in the logger's extras map. When provided, the effective context becomes '$parentContext > $context' — useful when chaining validate calls across nested models to show the full path to the failing item (e.g. UserPage.data[2] > MetaModel).

When verbose logging is enabled via configure or silence, a dart:developer trace is emitted on each passing call.

Example log output:

[ProductListing] JSON validation failed (2 errors):
  • Key 'productId' has invalid type. Expected: int; Actual: String.
  • Missing required key 'sku'.

Implementation

static JsonValidationResult validate({
  required Map<String, dynamic> json,
  required Map<String, List<Type?>?> expectedTypes,
  Set<String> optional = const <String>{},
  bool strict = false,
  bool warnUnexpected = false,
  bool escalate = false,
  String context = 'UnknownModel',
  String? parentContext,
  Map<String, bool Function(Object?)>? validators,
}) {
  final StackTrace stackTrace = StackTrace.current;
  assert(
    !(strict && warnUnexpected),
    'JsonSentinel.validate: strict and warnUnexpected are mutually exclusive — '
    'use strict to fail on unexpected keys, or warnUnexpected to log without failing, not both.',
  );
  final String effectiveContext = parentContext != null ? '$parentContext > $context' : context;
  final ({List<String> errors, List<String> warnings}) core = _validateCore(
    json: json,
    expectedTypes: expectedTypes,
    optional: optional,
    strict: strict,
    warnUnexpected: warnUnexpected,
    validators: validators,
  );

  if (core.warnings.isNotEmpty) {
    final String warnCount = '${core.warnings.length} unexpected key${core.warnings.length == 1 ? '' : 's'}';
    final String warnBullets = core.warnings.map((String w) => '  • $w').join('\n');
    _log(
      '[$effectiveContext] JSON validation warning ($warnCount):\n$warnBullets',
      stackTrace: stackTrace,
      extras: <String, Object?>{'context': effectiveContext, 'json_preview': _jsonPreview(json)},
      escalate: false,
    );
    if (_verbose) {
      developer.log(
        '[$effectiveContext] validate() warned — ${core.warnings.length} unexpected key(s).',
        name: 'JsonSentinel',
      );
    }
  }

  if (core.errors.isNotEmpty) {
    final String count = '${core.errors.length} error${core.errors.length == 1 ? '' : 's'}';
    final String bullets = core.errors.map((String e) => '  • $e').join('\n');
    _log(
      '[$effectiveContext] JSON validation failed ($count):\n$bullets',
      stackTrace: stackTrace,
      extras: <String, Object?>{'context': effectiveContext, 'json_preview': _jsonPreview(json)},
      escalate: escalate,
    );
    if (_verbose) {
      developer.log(
        '[$effectiveContext] validate() failed — ${core.errors.length} error(s).',
        name: 'JsonSentinel',
      );
    }
    return JsonValidationResult.failure(core.errors);
  }

  if (_verbose && core.warnings.isEmpty) {
    developer.log(
      '[$effectiveContext] validate() passed — ${expectedTypes.length} key(s) checked.',
      name: 'JsonSentinel',
    );
  }
  return JsonValidationResult.success;
}