validart 1.0.0
validart: ^1.0.0 copied to clipboard
A type-safe validation library for Dart, inspired by Zod. Supports parse/safeParse, transforms, coercion, schema composition, and structured errors.
Validart #
A type-safe validation library for Dart, inspired by Zod.
Built for chaining, schema composition, i18n, and extensibility. Includes validators for emails, phone numbers, dates, and more.
Installation #
dart pub add validart
import 'package:validart/validart.dart';
Basic Usage #
// No instantiation needed — V is a static class
final schema = V.string().email();
schema.validate('user@example.com'); // true
schema.validate('invalid'); // false
// Get structured errors
final errors = schema.errors('invalid');
// [VError(code: invalid_email, message: Invalid email address)]
// Parse — throws on failure
final value = schema.parse('user@example.com'); // 'user@example.com'
// SafeParse — never throws
final result = schema.safeParse('invalid');
if (result case VFailure(:final errors)) {
print(errors.first.message);
}
Types #
String #
V.string()
.notEmpty()
.min(5)
.max(100)
.email();
Available: notEmpty, min, max, length, email, url, uuid, ip, pattern, date, time, contains, startsWith, endsWith, equals, alpha, alphanumeric, slug, password, jwt, card, phone.
Pre-processing: trim, toLowerCase, toUpperCase — always run before validation, regardless of chain order.
Int #
V.int()
.min(0)
.max(100)
.even();
Available: min, max, positive, negative, between, multipleOf, even, odd, prime.
Double #
V.double()
.positive()
.finite();
Available: min, max, positive, negative, between, multipleOf, finite, decimal, integer.
Bool #
V.bool().isTrue();
Available: isTrue, isFalse.
Date #
V.date()
.after(DateTime(2024, 1, 1))
.weekday();
Available: after, before, between, weekday, weekend.
Map (Structured Objects) #
Validates Map<String, dynamic> with a schema:
final userSchema = V.map({
'name': V.string().min(1),
'email': V.string().email(),
'age': V.int().min(0).nullable(),
});
userSchema.validate({'name': 'Alice', 'email': 'a@b.com'}); // true
Errors include field paths:
final errors = userSchema.errors({'name': '', 'email': 'bad'});
// [VError(code: string.too_small, path: [name]), VError(code: invalid_email, path: [email])]
Schema Composition #
final base = V.map({'name': V.string(), 'email': V.string().email()});
base.pick(['name']); // only name
base.omit(['email']); // everything except email
base.extend({'password': V.string().min(8)}); // add fields
base.merge(otherSchema); // combine two schemas
base.partial(); // all fields nullable
base.strict(); // reject unknown keys
base.passthrough(); // allow unknown keys
Cross-Field Validation #
V.map({
'password': V.string().min(8),
'confirm': V.string(),
}).equalFields('password', 'confirm');
Custom Field Validation #
V.map({
'age': V.int(),
}).refineField(
(data) => (data['age'] as int) >= 18,
path: 'age',
message: 'Must be at least 18',
);
Conditional Validation #
V.map({
'type': V.string(),
'cnpj': V.string().nullable(),
}).when('type', equals: 'company', then: {
'cnpj': V.string().min(14),
});
Array of Maps #
V.map({'name': V.string().min(1)}).array();
Object (Entity Validation) #
Validates class instances with type-safe field extraction:
final schema = V.object<User>(
configure: (o) => o
.field('name', (u) => u.name, V.string().min(1))
.field('email', (u) => u.email, V.string().email()),
);
schema.validate(user); // true
Array #
final schema = V.string().email().array()
.min(1)
.unique();
schema.validate(['a@b.com', 'c@d.com']); // true
Errors include the array index in the path:
final errors = schema.errors(['a@b.com', 'bad']);
// [VError(code: invalid_email, path: [1])]
Available: min, max, unique, contains.
Other Types #
Enum #
V.enm(Status.values).validate(Status.active); // true
Literal #
V.literal('admin').validate('admin'); // true
V.literal('admin').validate('user'); // false
Union #
V.union([V.string().email(), V.int().min(1)]);
Coercion #
Converts input types automatically:
V.coerce.int().parse('42'); // 42
V.coerce.double().parse('3.14'); // 3.14
V.coerce.string().parse(42); // '42'
V.coerce.bool().parse('true'); // true
V.coerce.date().parse('2024-01-15'); // DateTime
Pipeline #
The validation pipeline runs in three phases:
- Pre-processing — normalizes the value before validation (
trim,toLowerCase,toUpperCase). The order in the chain does not matter — these always run first. - Validation — checks constraints on the normalized value (
email,min,max, etc.). - Post-processing — transforms the validated value (
transform<O>()). Only runs if validation passes.
// Both are equivalent — trim always runs before email validation:
V.string().trim().email();
V.string().email().trim();
Transform #
Change the output type (post-processing phase):
final schema = V.string().transform<int>((s) => s.length);
schema.parse('hello'); // 5
Preprocess #
Transform the raw input before type checking — runs before everything else, including coercion and null checks:
final schema = V.string()
.preprocess((v) => v?.toString().trim() ?? '');
schema.parse(42); // '42'
Modifiers #
Available on all types:
V.string().nullable(); // allows null
V.string().defaultValue('N/A'); // uses default when null
V.string().refine( // custom validation
(v) => v.contains('@'),
message: 'Must contain @',
);
Form Errors #
Convert errors to a map for Flutter forms:
final result = schema.safeParse(data);
if (result case VFailure(:final errors)) {
final map = result.toMap();
// {'email': 'Invalid email address', 'name': 'Required'}
}
i18n (Internationalization) #
Set translations using VLocale:
V.setLocale(const VLocale({
'required': 'Campo obrigatório',
'invalid_email': 'Email inválido',
'too_small': 'Mínimo de {min} caracteres',
}));
Only override what you need — everything else falls back to English defaults. Switch locale at runtime:
V.setLocale(const VLocale(ptBrTranslations));
V.setLocale(const VLocale(esTranslations));
V.setLocale(const VLocale()); // reset to English
Per-validator overrides bypass the locale:
V.string().min(3, message: (n) => 'At least $n chars');
Use V.t() to translate manually:
V.t('too_small', {'min': 3}); // 'Mínimo de 3 caracteres'
Error codes are defined in VCode:
VCode.required // 'required'
VCode.invalidEmail // 'invalid_email'
VCode.stringTooSmall // 'string.too_small'
VCode.numberTooSmall // 'number.too_small'
// ... see VCode for all codes
Extensibility #
The add() method is public, so external packages can add validators:
// In a package like validart_br:
class CpfValidator extends Validator<String> {
const CpfValidator();
@override
String get code => 'invalid_cpf';
@override
Map<String, dynamic>? validate(String value) =>
_isValid(value) ? null : {};
}
extension VStringBr on VString {
VString cpf({String? message}) {
add(const CpfValidator(), message: message);
return this;
}
}
License #
See LICENSE for details.