Declarative form validation with composable rules and JSON schemas
dart pub add philiprehberger_form_validatorDeclarative form validation with composable rules and JSON schemas
Add to your pubspec.yaml:
dependencies:
philiprehberger_form_validator: ^0.4.0
Then run:
dart pub get
import 'package:philiprehberger_form_validator/form_validator.dart';
final schema = FormSchema({
'name': [Rules.required(), Rules.minLength(2)],
'email': [Rules.required(), Rules.email()],
});
final result = schema.validate({
'name': 'Alice',
'email': 'alice@example.com',
});
print(result.isValid); // true
Rules.required()
Rules.email()
Rules.url()
Rules.minLength(3)
Rules.maxLength(100)
Rules.pattern(RegExp(r'^\d+$'))
Rules.numeric()
Rules.between(1, 100)
Rules.equals('password') // cross-field comparison
Rules.oneOf(['a', 'b', 'c'])
Rules.inRange(1, 100)
Rules.date()
Rules.dateAfter(DateTime(2026, 1, 1))
Rules.dateBefore(DateTime(2026, 12, 31))
Rules.minItems(1)
Rules.maxItems(10)
Rules.custom((v) => v != null, message: 'Required')
final schema = FormSchema.fromJson({
'email': ['required', 'email'],
'name': ['required', 'minLength:3', 'maxLength:100'],
'age': ['numeric', 'between:18,120'],
});
final schema = FormSchema({
'password': [Rules.required(), Rules.minLength(8)],
'confirm': [Rules.required(), Rules.equals('password')],
});
final result = schema.validate({
'password': 'secret123',
'confirm': 'secret123',
});
print(result.isValid); // true
final schema = FormSchema({
'country': [Rules.required()],
'state': [Rules.when((data) => data['country'] == 'US', Rules.required())],
});
final result = schema.validate({'country': 'US'});
print(result.hasError('state')); // true — required only when country is US
// All must pass
final strict = Rules.all([Rules.required(), Rules.minLength(8)]);
// Any can pass
final flexible = Rules.any([Rules.email(), Rules.url()]);
final schema = FormSchema({'username': [Rules.required()]});
final result = await schema.validateAsync(
{'username': 'taken'},
asyncValidators: [
MapEntry('username', AsyncFieldValidator(
'Username already taken',
(value) async => value != 'taken', // e.g. check server
)),
],
);
print(result.isValid); // false
final schema = FormSchema.nested(
{
'name': [Rules.required()],
},
nestedSchemas: {
'address': FormSchema({
'city': [Rules.required()],
'zip': [Rules.required(), Rules.pattern(RegExp(r'^\d{5}$'))],
}),
},
);
final result = schema.validateNested({
'name': 'Alice',
'address': {'city': '', 'zip': 'bad'},
});
print(result.hasError('address.city')); // true
print(result.hasError('address.zip')); // true
// Extract nested errors
final addressErrors = result.nested('address');
print(addressErrors.errorsFor('city')); // [This field is required]
final schema = FormSchema({
'birthday': [Rules.required(), Rules.date()],
'startDate': [Rules.dateAfter(DateTime(2026, 1, 1))],
'endDate': [Rules.dateBefore(DateTime(2026, 12, 31))],
});
final schema = FormSchema({
'tags': [Rules.minItems(1), Rules.maxItems(10)],
});
class SpanishMessages extends MessageProvider {
@override
String message(String ruleKey, Map<String, dynamic> params) {
switch (ruleKey) {
case 'required':
return 'Campo obligatorio';
case 'email':
return 'Correo electronico invalido';
default:
return 'Error de validacion';
}
}
}
// Set globally before creating rules
MessageProvider.setProvider(SpanishMessages());
final rule = Rules.required();
print(rule.validate(null)); // Campo obligatorio
// Reset to English defaults
MessageProvider.resetProvider();
final result = schema.validate(data);
result.isValid; // true if no errors
result.hasError('email'); // check specific field
result.errorsFor('email'); // list of error messages
result.allErrors; // flat list of all errors
result.errorCount; // total error count
| Class | Description |
|---|---|
FieldValidator | Single validation rule with message and test function |
Rules | Static factory methods for built-in validators |
FormSchema | Schema defining validators per field, validates form data maps |
FormSchema.fromJson() | Create schema from JSON-like rule descriptor map |
ValidationResult | Result object with errors, field queries, and counts |
CrossFieldValidator | Validator that compares against another field's value |
AsyncFieldValidator | Async validation rule (e.g. server-side checks) |
Rules.when() | Conditional validator based on form data |
Rules.inRange() | Inclusive numeric range validation |
Rules.all() | Composite validator requiring all rules to pass |
Rules.any() | Composite validator requiring any rule to pass |
MessageProvider | Abstract class for localizable error messages |
DefaultMessageProvider | Built-in English message provider |
FormSchema.nested() | Schema with support for nested object validation |
FormSchema.validateNested() | Validates data including nested objects with dot-path keys |
Rules.date() | Validates date string format |
Rules.dateAfter(min) | Date must be on or after min |
Rules.dateBefore(max) | Date must be on or before max |
Rules.minItems(min) | Collection must have at least min items |
Rules.maxItems(max) | Collection must have at most max items |
ValidationResult.nested() | Extracts errors for a nested prefix |
dart pub get
dart analyze --fatal-infos
dart test
If you find this project useful: