Custom Validation Rules
While Ack provides many built-in validation rules, you can extend them with your own value-level constraints or object-level refinement logic.
Prerequisites
- Validation Rules: Built-in constraints and how they work
- Schema Types: Different schema types and their behavior
- Error Handling: How validation errors are structured
Creating a value constraint
To add reusable validation that only depends on the field value, implement a Constraint<T> that mixes in Validator<T>.
import 'package:ack/ack.dart';
class IsPositiveConstraint extends Constraint<double> with Validator<double> {
IsPositiveConstraint()
: super(
constraintKey: 'is_positive',
description: 'Number must be positive',
);
@override
bool isValid(double value) => value > 0;
@override
String buildMessage(double value) => 'Number must be positive';
}
final priceSchema = Ack.double().constrain(IsPositiveConstraint());
print(priceSchema.safeParse(10.5).isOk); // true
print(priceSchema.safeParse(-5.0).isFail); // trueCross-field rules with .refine()
When validation depends on multiple fields, use .refine() on the parent object schema.
final signUpSchema = Ack.object({
'password': Ack.string().minLength(8),
'confirmPassword': Ack.string().minLength(8),
}).refine(
(data) => data['password'] == data['confirmPassword'],
message: 'Passwords do not match',
);
final result = signUpSchema.safeParse({
'password': 'pass1234',
'confirmPassword': 'different',
});
print(result.isFail); // trueOverriding error messages
The optional message parameter on .constrain() lets you customize the failure message.
final schema = Ack.double()
.constrain(IsPositiveConstraint(), message: 'Price must be greater than zero.');
final result = schema.safeParse(-10.0);
if (result.isFail) {
print(result.getError().message); // Price must be greater than zero.
}Organizing reusable constraints
Place frequently used constraints in utility files so they can be shared across schemas.
// file: validation/constraints.dart
import 'package:ack/ack.dart';
class IsPositiveConstraint extends Constraint<double> with Validator<double> {
// ...
}
// file: schemas/user_schema.dart
import 'package:ack/ack.dart';
import '../validation/constraints.dart';
final userSchema = Ack.object({
'age': Ack.integer(),
'salary': Ack.double().constrain(IsPositiveConstraint()),
});When to use custom logic
- Complex business rules: Domain-specific checks that built-ins don’t cover.
- Cross-field relationships: Comparing password and confirmation fields, for example.
- Reusable patterns: Common rules applied across multiple schemas.
- External service checks: Validating against APIs or databases (beware of latency).
Chaining built-in constraints is often enough for simple cases. Use custom constraints or refinements when you need extra flexibility.
Next steps
- Common Recipes — custom constraints in real-world schemas
- Flutter Form Validation — surface custom messages in forms
- Error Handling — read
SchemaConstraintsError
Last updated on