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:

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.