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
Before creating custom validation, you should understand:
- 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).isFail); // true
Cross-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); // true
Overriding 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.');
print(schema.safeParse(-10).getError().toString()); // 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: e.g. comparing password and confirmation fields.
- Reusable Patterns: Common rules you apply across multiple schemas.
- External Service Checks: Validating against APIs or databases (beware of performance/latency).
For simple cases, chaining built-in constraints is often enough. Reach for custom constraints/refinements when you need extra flexibility.