dox_validation 3.0.0-alpha.2
dox_validation: ^3.0.0-alpha.2 copied to clipboard
Typed input for Dart. DTO classes with generated checks, fluent schemas like Zod, JSON Schema export and the Standard Schema interface.
dox_validation #
Typed input for Dart.
- DTO classes: a class with
@Dto()and rule annotations.dox_builderwrites the checks as plain Dart code. - Fluent schemas, like Zod, for input you only know at runtime.
- All problems come back at once, each with a path, a code and a message.
- Every schema implements the Standard Schema interface (
StandardSchema). - Every schema can export JSON Schema (draft 2020-12, OpenAPI 3.1) with
toJsonSchema(). - No dependencies.
Quick start #
import 'package:dox_validation/dox_validation.dart';
final ObjectSchema createUser = v.object(<String, Schema<Object?>>{
'name': v.string().trim().min(3).max(50),
'email': v.string().email(),
'age': v.coerce.int().min(18).optional(),
'role': v.enumOf<String>(<String>['admin', 'user']).defaultTo('user'),
'tags': v.list(v.string()).max(5),
});
// Throws ValidationException when the input is not valid.
Map<String, dynamic> data = createUser.parse(input);
// Never throws for bad input.
ParseResult<Map<String, dynamic>> result = createUser.safeParse(input);
if (!result.success) {
print(result.errors); // {'email': 'The email is not a valid email'}
for (ValidationIssue issue in result.issues) {
print('${issue.path} ${issue.code} ${issue.message}');
}
}
Schemas #
| Schema | Notes |
|---|---|
v.string() |
min, max, length, nonEmpty, email, url, uuid, ip, datetime, regex, startsWith, endsWith, includes, alpha, alphaDash, alphaNumeric, trim, toLowerCase, toUpperCase |
v.int(), v.num() |
min, max, gt, lt, positive, nonNegative, multipleOf |
v.bool() |
|
v.date() |
DateTime only. min, max |
v.enumOf([...]), v.literal(x) |
|
v.list(item) |
min, max, nonEmpty |
v.object({...}) |
strict, passthrough, strip, pick, omit, extend, partial |
v.map(values, keys: ...) |
any keys |
v.union([...]) |
first match wins |
v.any(), v.file() |
file() takes a FileInput. image, extensions |
On every schema: optional(), nullable(), nullish(), defaultTo(x),
transform(fn), refine(fn), refineAsync(fn), superRefine(fn),
use(rule), describe(text), example(x).
optional(): the key may be missing.nullis still an error.nullable(): the value may benull. The key must be there.nullish(): both.defaultTo(x):defaultis a reserved word in Dart, so the name is different.
Coercion #
Query strings and form data are text. v.coerce turns text into the right type first.
v.coerce.int().parse('42'); // 42
v.coerce.num().parse('4.5'); // 4.5
v.coerce.bool().parse('on'); // true (true/1/yes/on, false/0/no/off)
v.coerce.date().parse('2024-01-31');
v.coerce.string().parse(5); // '5'
Async checks #
final StringSchema email = v.string().email().refineAsync(
(String value) async {
return !(await users.emailExists(value));
},
message: 'The {field} is already taken',
);
await email.parseAsync('a@dox.dev');
parse() throws a StateError when a schema has async checks. Use parseAsync() or safeParseAsync().
Object checks #
final ObjectSchema signUp = v.object(<String, Schema<Object?>>{
'password': v.string().min(8),
'confirm': v.string(),
}).refine((Map<String, dynamic> data) {
return data['password'] == data['confirm'];
}, message: 'Passwords do not match', path: <Object>['confirm']);
Custom rules #
Extend Rule for a check you want to reuse. Add an extension method to make it look like a built-in method.
class SlugRule extends Rule<String> {
SlugRule({super.message});
@override
String get name {
return 'slug';
}
@override
String get defaultMessage {
return 'The {field} must be a slug';
}
@override
bool check(String value, RuleContext context) {
return RegExp(r'^[a-z0-9-]+$').hasMatch(value);
}
// Optional: shown in toJsonSchema().
@override
Map<String, dynamic> toJsonSchema() {
return <String, dynamic>{'pattern': r'^[a-z0-9-]+$'};
}
}
extension SlugMethods on StringSchema {
StringSchema slug({String? message}) {
return use(SlugRule(message: message));
}
}
v.string().min(3).slug();
- For a database check, extend
AsyncRuleand implementcheckAsync. - Add values for the message with the
paramsgetter, like{'count': 3}for{count}. RuleContextgivespath,field,root(the whole input) andlookup('other.field').
Messages #
Each issue has a code, like required or min_length (see IssueCodes).
Messages are found in this order:
- The message on the rule:
.min(3, message: 'Too short'). messages['path.code'], likeemail.requiredoritems.*.name.required.messages['code'], likerequired.- The default English text (see
DefaultMessages).
Placeholders: {field} (last key in the path), {value}, and rule values like {min} and {max}. Old dox %s placeholders also work.
// Global messages
v.messages.setMessages(<String, String>{'required': 'Please fill in {field}'});
// For one call
createUser.safeParse(input, messages: MessageResolver(messages: <String, String>{
'email.required': 'We need your email',
}));
// For i18n, extend MessageResolver and override resolve().
v.messages = MyGermanResolver();
DTOs #
part 'create_user.dto.g.dart';
@Dto()
class CreateUser {
@Length(min: 3, max: 50)
@Trim()
final String name;
@Email()
final String email;
const CreateUser({required this.name, required this.email});
static const DtoSchema<CreateUser> schema = $CreateUserDto;
}
CreateUser user = await CreateUser.schema.validateMap(input);
Run dart run build_runner build (dox_builder is a dev dependency). See the
Validation guide and the
rules reference.
JSON Schema #
v.object(<String, Schema<Object?>>{
'email': v.string().email().describe('Login email'),
'age': v.int().min(18).nullable(),
'role': v.enumOf<String>(<String>['admin', 'user']).defaultTo('user'),
}).strict().toJsonSchema();
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email", "description": "Login email" },
"age": { "type": ["integer", "null"], "minimum": 18 },
"role": { "type": "string", "enum": ["admin", "user"], "default": "user" }
},
"required": ["email", "age"],
"additionalProperties": false
}
Standard Schema #
Every schema implements StandardSchema<Object?, O>:
abstract class StandardSchema<I, O> {
String get vendor; // 'dox'
int get version; // 1
FutureOr<StandardResult<O>> validate(Object? value);
}
Code that only needs to validate can accept any StandardSchema, from dox or from another library.
validatereturns a plain result for sync schemas and aFuturefor async ones.- On success,
issuesis an empty list (the JS spec usesundefined).