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).

Common Constraints (Applicable to Multiple Types)

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(Constraint<T> constraint, {String? message})

Applies a custom Constraint<T> that also implements Validator<T>. See the Custom Validation guide.

String Constraints

Apply these to Ack.string() schemas.

minLength(int min)

Requires at least min characters.

Ack.string().minLength(5)

maxLength(int max)

Requires at most max characters.

Ack.string().maxLength(100)

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()

Requires a non-empty string. Equivalent to minLength(1).

Ack.string().notEmpty()

matches(String pattern, {String? example, String? message})

Requires the string to match a 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})

Requires pattern to appear somewhere in the string.

// 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]')

email()

Requires a valid email address format.

Ack.string().email()

date()

Requires a valid date string in YYYY-MM-DD format.

Ack.string().date()

datetime()

Requires a valid ISO 8601 date-time string.

Ack.string().datetime()

time()

Requires a valid time string in HH:MM:SS format.

Ack.string().time()

uri()

Requires a valid URI per RFC 3986.

Ack.string().uri()

uuid()

Requires a valid UUID per RFC 4122.

Ack.string().uuid()

ipv4()

Requires a valid IPv4 address.

Ack.string().ipv4()

ipv6()

Requires a valid IPv6 address.

Ack.string().ipv6()

enumString(List<String> allowedValues)

Requires the value to be one of allowedValues. Use this only for ad-hoc string lists. When a Dart enum exists, prefer Ack.enumValues(MyEnum.values) for type-safe validation.

Ack.enumString(['active', 'inactive', 'pending'])

literal(String value)

Requires an exact string match.

Ack.literal('admin')

startsWith(String prefix)

Requires the string to start with prefix.

Ack.string().startsWith('https://')

endsWith(String suffix)

Requires the string to end with suffix.

Ack.string().endsWith('.dart')

url()

Alias for uri(). Requires a valid URL.

Ack.string().url()

ip({int? version})

Requires a valid IP address. Set version to restrict to IPv4 or IPv6.

Ack.string().ip()          // Any IP (v4 or v6)
Ack.string().ip(version: 4) // IPv4 only
Ack.string().ip(version: 6) // IPv6 only

String Transformations

These methods transform the string value during parsing. They don't add validation constraints but modify the output value.

trim()

Removes leading and trailing whitespace from the string.

Ack.string().trim()
// "  hello  " → "hello"

toLowerCase()

Converts the string to lowercase.

Ack.string().toLowerCase()
// "HELLO" → "hello"

toUpperCase()

Converts the string to uppercase.

Ack.string().toUpperCase()
// "hello" → "HELLO"

Number Constraints (Int and Double)

Apply these to Ack.integer() and Ack.double() schemas.

min(num limit)

Requires a value >= limit (inclusive).

Ack.integer().min(0) // >= 0
Ack.double().min(0.0) // >= 0.0

max(num limit)

Requires a value <= limit (inclusive).

Ack.integer().max(100) // <= 100
Ack.double().max(100.0) // <= 100.0

greaterThan(num limit)

Requires a value strictly > limit (exclusive).

Ack.integer().greaterThan(0) // > 0
Ack.double().greaterThan(0.0) // > 0.0

lessThan(num limit)

Requires a value strictly < limit (exclusive).

Ack.integer().lessThan(100) // < 100
Ack.double().lessThan(100.0) // < 100.0

multipleOf(num factor)

Requires a value that is a multiple of factor.

Ack.integer().multipleOf(5) // Must be divisible by 5
Ack.double().multipleOf(0.5) // Use factors that avoid floating point rounding issues

positive()

Requires a value greater than 0.

Ack.integer().positive() // > 0
Ack.double().positive() // > 0.0

negative()

Requires a value less than 0.

Ack.integer().negative() // < 0
Ack.double().negative() // < 0.0

safe() (Integer only)

Requires an integer within JavaScript's safe range (-2^53+1 to 2^53-1).

Ack.integer().safe()

finite() (Double only)

Requires a finite number (rejects infinity and NaN).

Ack.double().finite()

List Constraints

Apply these to Ack.list() schemas.

minLength(int min)

Requires at least min items.

Ack.list(Ack.string()).minLength(1)

maxLength(int max)

Requires at most max items.

Ack.list(Ack.integer()).maxLength(10)

length(int count)

Requires exactly count items.

Ack.list(Ack.boolean()).length(5)

notEmpty()

Requires a non-empty list. Equivalent to minLength(1).

Ack.list(Ack.object({})).notEmpty()

unique()

Requires all items to be 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.042)
  • Integers → no conversion

Double Schema:

  • Numeric strings → parsed double ("3.14"3.14)
  • Integers → converted to double (4242.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: