Validation Rules
Ack provides a rich set of built-in validation rules (constraints) that you can chain onto schema types to enforce specific requirements. Built-in constraints provide default error messages. For custom error messages, use custom constraints (see Custom Validation Guide).
nullable()
Allows the value to be null
in addition to the type's constraints.
final optionalName = Ack.string().nullable();
final optionalAge = Ack.integer().nullable();
constrain(SchemaConstraint constraint)
Applies a custom SchemaConstraint
. See the Custom Validation guide.
String Constraints
Apply these to Ack.string()
schemas.
Exact Length
To ensure a string has exactly a specific length, combine minLength()
and maxLength()
:
// String must be exactly 10 characters
Ack.string().minLength(10).maxLength(10)
notEmpty()
Ensures the string is not empty (''
). Equivalent to minLength(1)
.
Ack.string().notEmpty()
matches(String pattern, {String? example, String? message})
Ensures the string matches the given regular expression pattern
.
Important: Patterns are NOT automatically anchored. The pattern will match if found anywhere in the string (substring matching). To require full-string matching, explicitly add anchors: ^...$
// Simple alphanumeric pattern (full-string match with anchors)
Ack.string().matches(r'^[a-zA-Z0-9]+$')
// UUID pattern (full-string match with anchors)
Ack.string()
.matches(r'^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$')
// Without anchors - matches substring (usually not what you want!)
Ack.string().matches(r'[0-9]+') // Matches "abc123xyz" ⚠️
contains(String pattern, {String? example, String? message})
Ensures the string contains the given pattern anywhere within it.
// Password must contain at least one uppercase letter
Ack.string().contains(r'[A-Z]')
// Password must contain at least one digit
Ack.string().contains(r'[0-9]')
enumString(List<String> allowedValues)
Ensures the string is one of the allowedValues
.
Ack.enumString(['active', 'inactive', 'pending'])
Number Constraints (Int and Double)
Apply these to Ack.integer()
and Ack.double()
schemas.
min(num limit)
Ensures the number is greater than or equal to the limit
.
Ack.integer().min(0) // >= 0
Ack.double().min(0.0) // >= 0.0
max(num limit)
Ensures the number is less than or equal to the limit
.
Ack.integer().max(100) // <= 100
Ack.double().max(100.0) // <= 100.0
multipleOf(num factor)
Ensures the number is a multiple of the factor
.
Ack.integer().multipleOf(5) // Must be divisible by 5
Ack.double().multipleOf(0.5) // Use factors that avoid floating point rounding issues
positive()
Ensures the number is positive (greater than 0).
Ack.integer().positive() // > 0
Ack.double().positive() // > 0.0
negative()
Ensures the number is negative (less than 0).
Ack.integer().negative() // < 0
Ack.double().negative() // < 0.0
List Constraints
Apply these to Ack.list()
schemas.
notEmpty()
Ensures the list is not empty. Equivalent to minLength(1)
.
Ack.list(Ack.object({})).notEmpty()
unique()
Ensures all items in the list are unique. Uses deep structural equality, so nested maps/lists are compared by value.
Ack.list(Ack.string()).unique()
Strict Parsing
By default, Ack performs type coercion for primitive types. Use .strictParsing()
to disable coercion and require exact type matches.
Default Behavior (Type Coercion Enabled)
Without strict parsing, Ack accepts compatible types and converts them:
// String schema accepts numbers and converts them
final stringSchema = Ack.string();
stringSchema.safeParse(123); // ✅ OK: converts to "123"
stringSchema.safeParse(true); // ✅ OK: converts to "true"
stringSchema.safeParse('hello'); // ✅ OK: already a string
// Integer schema accepts numeric strings and converts them
final intSchema = Ack.integer();
intSchema.safeParse('42'); // ✅ OK: converts to 42
intSchema.safeParse(42); // ✅ OK: already an integer
intSchema.safeParse(42.0); // ✅ OK: converts to 42
Strict Parsing Behavior
With strict parsing enabled, only exact type matches are accepted:
// String schema rejects non-strings
final strictStringSchema = Ack.string().strictParsing();
strictStringSchema.safeParse('hello'); // ✅ OK: exact match
strictStringSchema.safeParse(123); // ❌ FAIL: TypeMismatchError
strictStringSchema.safeParse(true); // ❌ FAIL: TypeMismatchError
// Integer schema rejects strings and doubles
final strictIntSchema = Ack.integer().strictParsing();
strictIntSchema.safeParse(42); // ✅ OK: exact match
strictIntSchema.safeParse('42'); // ❌ FAIL: TypeMismatchError
strictIntSchema.safeParse(42.0); // ❌ FAIL: TypeMismatchError
When to Use Strict Parsing
Use strict parsing when you need to:
- Validate API requests where types must match exactly
- Distinguish between types in union schemas (e.g., string "123" vs integer 123)
- Enforce type discipline in strongly typed contexts
- Prevent unexpected conversions that might hide bugs
// Union type that distinguishes strings from numbers
final stringOrNumber = Ack.anyOf([
Ack.string().strictParsing(), // Only accepts actual strings
Ack.integer(), // Only accepts integers
]);
stringOrNumber.safeParse('42'); // ✅ Matches string schema
stringOrNumber.safeParse(42); // ✅ Matches integer schema
Type Coercion Rules
When strict parsing is disabled (default), Ack applies these coercion rules:
String Schema:
- Numbers → string representation (
123
→"123"
) - Booleans → string representation (
true
→"true"
) - Strings → no conversion
Integer Schema:
- Numeric strings → parsed integer (
"42"
→42
) - Doubles without decimals → converted (
42.0
→42
) - Integers → no conversion
Double Schema:
- Numeric strings → parsed double (
"3.14"
→3.14
) - Integers → converted to double (
42
→42.0
) - Doubles → no conversion
Boolean Schema:
- Strings "true"/"false" → corresponding boolean
- Numbers 1/0 → true/false
- Booleans → no conversion
Combining Constraints
You can chain multiple constraints together. Ack evaluates them in the order you apply them.
final usernameSchema = Ack.string()
.minLength(3) // First: check min length
.maxLength(20) // Second: check max length
.matches(r'[a-z0-9_]+') // Third: check pattern (lowercase alphanumeric/underscore)
.notEmpty(); // Redundant if minLength(>0) is used, but illustrates chaining
final quantitySchema = Ack.integer()
.min(1) // Must be at least 1
.max(100) // Must be at most 100
.multipleOf(1); // Must be an integer (redundant for Ack.integer)
Next Steps
Now that you understand validation rules, explore these related topics:
- Error Handling: Learn how to handle and display validation errors
- Custom Validation: Create custom constraints and refinement logic
- Schema Types: Explore all available schema types and their operations
- Common Recipes: See practical validation patterns and solutions
- Flutter Forms: Integrate validation with Flutter form widgets