validateJson function

void validateJson(
  1. dynamic instance,
  2. Map<String, dynamic> schema, {
  3. String at = r'$',
})

Implementation

void validateJson(
  dynamic instance,
  Map<String, dynamic> schema, {
  String at = r'$',
}) {
  bool isType(dynamic v, String t) => switch (t) {
    'object' => v is Map,
    'array' => v is List,
    'string' => v is String,
    'number' => v is num,
    'integer' => v is int,
    'boolean' => v is bool,
    'null' => v == null,
    _ => true, // unknown type keyword => skip
  };

  // enum
  final enums = schema['enum'];
  if (enums is List && !enums.contains(instance)) {
    throw JsonSchemaValidationException(
      '$at: value $instance not in enum $enums',
    );
  }

  // oneOf
  final oneOf = schema['oneOf'];
  if (oneOf is List) {
    var ok = false;
    for (final s in oneOf.whereType<Map>()) {
      try {
        validateJson(instance, s.cast<String, dynamic>(), at: at);
        ok = true;
        break;
      } catch (_) {}
    }
    if (!ok) {
      throw JsonSchemaValidationException(
        '$at: value does not match any oneOf schemas',
      );
    }
    return;
  }

  // anyOf
  final anyOf = schema['anyOf'];
  if (anyOf is List) {
    var ok = false;
    for (final s in anyOf.whereType<Map>()) {
      try {
        validateJson(instance, s.cast<String, dynamic>(), at: at);
        ok = true;
        break;
      } catch (_) {}
    }
    if (!ok) {
      throw JsonSchemaValidationException(
        '$at: value does not match any anyOf schemas',
      );
    }
    return;
  }

  // allOf
  final allOf = schema['allOf'];
  if (allOf is List) {
    for (final s in allOf.whereType<Map>()) {
      validateJson(instance, s.cast<String, dynamic>(), at: at);
    }
  }

  // type
  final type = schema['type'];
  if (type is String) {
    if (!isType(instance, type)) {
      throw JsonSchemaValidationException(
        '$at: expected $type, got ${instance.runtimeType}',
      );
    }
  } else if (type is List) {
    final ok = type.whereType<String>().any((t) => isType(instance, t));
    if (!ok) {
      throw JsonSchemaValidationException(
        '$at: expected one of $type, got ${instance.runtimeType}',
      );
    }
  }

  // object props
  final props = schema['properties'];
  if (type == 'object' || props is Map) {
    if (instance is! Map) {
      throw JsonSchemaValidationException(
        '$at: expected object, got ${instance.runtimeType}',
      );
    }

    final properties = (props is Map)
        ? props.cast<String, dynamic>()
        : const <String, dynamic>{};
    final required =
        (schema['required'] as List?)?.whereType<String>().toList() ??
        const <String>[];

    for (final k in required) {
      if (!instance.containsKey(k)) {
        throw JsonSchemaValidationException(
          '$at: missing required property "$k"',
        );
      }
    }

    final additional = schema['additionalProperties'];
    for (final entry in instance.entries) {
      final k = entry.key;
      final v = entry.value;
      final sub = properties[k];
      if (sub is Map) {
        validateJson(v, sub.cast<String, dynamic>(), at: '$at.$k');
      } else if (additional == false) {
        throw JsonSchemaValidationException(
          '$at: additional property "$k" not allowed',
        );
      } else if (additional is Map) {
        validateJson(v, additional.cast<String, dynamic>(), at: '$at.$k');
      }
    }
  }

  // array items
  final items = schema['items'];
  if (type == 'array' || items != null) {
    if (instance is! List) {
      throw JsonSchemaValidationException(
        '$at: expected array, got ${instance.runtimeType}',
      );
    }
    if (items is Map) {
      for (var i = 0; i < instance.length; i++) {
        validateJson(instance[i], items.cast<String, dynamic>(), at: '$at[$i]');
      }
    }
  }
}