validate method
Validate data against this schema.
Implementation
SchemaResponse validate(Map<String, dynamic> data) {
// Work on a defensive copy; never mutate caller input.
final input = Map<String, dynamic>.from(data);
final sanitized = <String, dynamic>{
for (final entry in data.entries)
if (allowUnknownKeys || _schema.containsKey(entry.key))
entry.key: entry.value,
};
final errors = <String, String>{};
for (final key in _schema.keys) {
final rules = _schema[key]!;
final hasKey = input.containsKey(key);
final value = input[key];
// ---------- required / conditional required ----------
final isRequired = _asBool(rules[Rules.required]) ?? false;
final requiredIf = rules[Rules.requiredIf];
final requiredWhen = rules[Rules.requiredWhen];
final requiredBecauseIf = _requiredIf(requiredIf, input);
final requiredBecauseWhen = _requiredWhen(requiredWhen, input);
final mustExist = isRequired || requiredBecauseIf || requiredBecauseWhen;
if (!hasKey || value == null) {
if (mustExist) {
_putError(
errors,
key,
_msg(key, Rules.required, defaultMsg: '$key is required'),
);
sanitized.remove(key);
continue;
} else {
// no value, not required => skip further checks
sanitized.remove(key);
continue;
}
}
// ---------- notEmpty ----------
if (_asBool(rules[Rules.notEmpty]) == true) {
if ((value is String && value.trim().isEmpty) ||
(value is Iterable && value.isEmpty) ||
(value is Map && value.isEmpty)) {
_putError(
errors,
key,
_msg(key, Rules.notEmpty, defaultMsg: '$key must not be empty'),
);
sanitized.remove(key);
continue;
}
}
// ---------- type check ----------
final expectedType = rules[Rules.type];
if (expectedType != null) {
final ok = _isOfType(value, expectedType);
if (!ok) {
_putError(
errors,
key,
_msg(key, Rules.type, defaultMsg: '$key has invalid type'),
);
sanitized.remove(key);
continue;
}
}
// ---------- numeric bounds ----------
if (value is num) {
final min = rules[Rules.min];
if (min is num && value < min) {
_putError(
errors,
key,
_msg(key, Rules.min, defaultMsg: '$key is below minimum ($min)'),
);
}
final max = rules[Rules.max];
if (max is num && value > max) {
_putError(
errors,
key,
_msg(key, Rules.max, defaultMsg: '$key exceeds maximum ($max)'),
);
}
}
// ---------- string length bounds ----------
if (value is String) {
final minLen = rules[Rules.minLength];
if (minLen is int && value.length < minLen) {
_putError(
errors,
key,
_msg(
key,
Rules.minLength,
defaultMsg: '$key is too short (min $minLen)',
),
);
}
final maxLen = rules[Rules.maxLength];
if (maxLen is int && value.length > maxLen) {
_putError(
errors,
key,
_msg(
key,
Rules.maxLength,
defaultMsg: '$key is too long (max $maxLen)',
),
);
}
}
// ---------- list size bounds ----------
if (value is List) {
final minItems = rules[Rules.minItems];
if (minItems is int && value.length < minItems) {
_putError(
errors,
key,
_msg(
key,
Rules.minItems,
defaultMsg: '$key needs at least $minItems item(s)',
),
);
}
final maxItems = rules[Rules.maxItems];
if (maxItems is int && value.length > maxItems) {
_putError(
errors,
key,
_msg(
key,
Rules.maxItems,
defaultMsg: '$key allows at most $maxItems item(s)',
),
);
}
}
// ---------- format checks ----------
if (_asBool(rules[Rules.email]) == true) {
if (!(value is String && _emailRegex.hasMatch(value))) {
_putError(
errors,
key,
_msg(key, Rules.email, defaultMsg: '$key is not a valid email'),
);
}
}
if (_asBool(rules[Rules.phone]) == true) {
// E.164: +<country><number>, up to 15 digits (relaxed to allow local too)
if (!(value is String && _phoneE164Regex.hasMatch(value))) {
_putError(
errors,
key,
_msg(key, Rules.phone, defaultMsg: '$key is not a valid phone'),
);
}
}
if (_asBool(rules[Rules.url]) == true) {
final uri = value is String ? Uri.tryParse(value) : null;
final ok =
uri != null &&
['http', 'https'].contains(uri.scheme) &&
uri.host.isNotEmpty;
if (!ok) {
_putError(
errors,
key,
_msg(key, Rules.url, defaultMsg: '$key is not a valid URL'),
);
}
}
if (_asBool(rules[Rules.uuid]) == true) {
if (!(value is String && _uuidRegex.hasMatch(value))) {
_putError(
errors,
key,
_msg(key, Rules.uuid, defaultMsg: '$key is not a valid UUID'),
);
}
}
if (_asBool(rules[Rules.ipv4]) == true) {
if (!(value is String && _ipv4Regex.hasMatch(value))) {
_putError(
errors,
key,
_msg(key, Rules.ipv4, defaultMsg: '$key is not a valid IPv4'),
);
}
}
if (_asBool(rules[Rules.ipv6]) == true) {
if (!(value is String && _isIpv6(value))) {
_putError(
errors,
key,
_msg(key, Rules.ipv6, defaultMsg: '$key is not a valid IPv6'),
);
}
}
final pattern = rules[Rules.pattern];
if (pattern is String || pattern is RegExp) {
final rx = pattern is RegExp ? pattern : RegExp(pattern as String);
if (!(value is String && rx.hasMatch(value))) {
_putError(
errors,
key,
_msg(
key,
Rules.pattern,
defaultMsg: '$key does not match required pattern',
),
);
}
}
// ---------- enums / allowed set ----------
final allowed = rules[Rules.includes] ?? rules['enum'];
if (allowed is Iterable) {
final set = allowed.toSet();
if (!set.contains(value)) {
_putError(
errors,
key,
_msg(key, Rules.includes, defaultMsg: '$key must be one of $set'),
);
}
}
// ---------- equalsTo (confirm) ----------
final equalsTo = rules[Rules.equalsTo];
if (equalsTo is String) {
if (input[equalsTo] != value) {
_putError(
errors,
key,
_msg(key, Rules.equalsTo, defaultMsg: '$key must equal $equalsTo'),
);
}
}
// ---------- date & boundaries ----------
if (_asBool(rules[Rules.date]) == true) {
if (!(value is String && _isParsableDate(value))) {
_putError(
errors,
key,
_msg(key, Rules.date, defaultMsg: '$key is not a valid date'),
);
} else {
final dt = DateTime.parse(value);
final before = rules[Rules.before];
final after = rules[Rules.after];
if (before is String &&
_isParsableDate(before) &&
!(dt.isBefore(DateTime.parse(before)))) {
_putError(
errors,
key,
_msg(
key,
Rules.before,
defaultMsg: '$key must be before $before',
),
);
}
if (after is String &&
_isParsableDate(after) &&
!(dt.isAfter(DateTime.parse(after)))) {
_putError(
errors,
key,
_msg(key, Rules.after, defaultMsg: '$key must be after $after'),
);
}
}
}
// ---------- custom function ----------
final custom = rules[Rules.custom];
if (custom
is String? Function(dynamic value, Map<String, dynamic> data)) {
final msg = custom(value, input);
if (msg != null && msg.isNotEmpty) {
_putError(errors, key, _msg(key, Rules.custom, defaultMsg: msg));
}
}
// If any error exists for this key, remove from sanitized.
if (errors.containsKey(key)) {
sanitized.remove(key);
}
}
return SchemaResponse(
valid: errors.isEmpty,
data: Map.unmodifiable(input),
schema: Map.unmodifiable(sanitized),
errors: Map.unmodifiable(errors),
);
}