Schema Types in Ack

This guide covers all the built-in schema types in Ack and their specific validation options.

Basic Schema Types

String Schema

Create string validators with various constraints:

// Basic string schema
final nameSchema = Ack.string;

// String with length constraints
final usernameSchema = Ack.string
  .minLength(3)
  .maxLength(20)
  .matches(r'[a-zA-Z0-9_]+', example: 'user_123'); // Regex: letters, numbers, underscore

// Email validation
final emailSchema = Ack.string.isEmail();

// Enum values (select from options)
final roleSchema = Ack.string.isEnum(['admin', 'user', 'guest']);

// Note: For more constraints (e.g., regex patterns), see the [Validation](./validation.mdx) page.
// For custom constraints, see the [Custom Validation](../guides/custom-validation.mdx) page.

Number Schemas

Create validators for integer and decimal values:

// Integer validation
final ageSchema = Ack.int
  .min(0)
  .max(120);

// Double/decimal validation
final priceSchema = Ack.double
  .min(0.0, exclusive: true) // Must be greater than 0.0
  .multipleOf(0.01); // Allow only 2 decimal places

// Note: See the [Validation](./validation.mdx) page for more number constraints (e.g., positive, negative).

Boolean Schema

Simple true/false validation:

final isActiveSchema = Ack.boolean;

Date and Time Validation

Validate date and time values using specialized string constraints:

// Basic date validation (YYYY-MM-DD)
final birthdateSchema = Ack.string.isDate();

// Date-time validation (ISO 8601 format)
final appointmentSchema = Ack.string.isDateTime();

// For date range validation or custom formats, see the [Custom Validation](../guides/custom-validation.mdx) page.

Collection Schemas

List Schema

Validate arrays and lists containing elements of a specific schema type:

// List of strings
final tagsSchema = Ack.list(Ack.string);

// List with constraints on the list itself
final itemsSchema = Ack.list(Ack.string)
  .minItems(1)       // Must have at least 1 item
  .maxItems(10)      // Must have at most 10 items
  .uniqueItems();   // All items must be unique

// List of complex objects (e.g., a list of users)
final usersListSchema = Ack.list(
  Ack.object({
    'id': Ack.int.positive(),
    'name': Ack.string
  }, required: ['id', 'name'])
);

Map Schema (Using Object Schema)

While Ack doesn't have a dedicated Ack.map schema, you can validate key-value maps using an Ack.object with additionalProperties:

// Map with string keys to integer values (e.g., scores)
final scoresSchema = Ack.object(
  {}, // No predefined properties expected
  additionalProperties: Ack.int.min(0).max(100) // All values must be ints between 0-100
);

// Validate a scores map
final result = scoresSchema.validate({'math': 95, 'history': 88});
print(result.isOk); // true

final invalidResult = scoresSchema.validate({'english': 105}); // Value out of range
print(invalidResult.isOk); // false

Object Schema

Validate structured data objects with defined properties and types. This is one of the most common schema types.

// User object schema
final userSchema = Ack.object(
  {
    // Define properties and their schemas
    'name': Ack.string.minLength(1),
    'age': Ack.int.min(0),
    'email': Ack.string.isEmail(),
    // Nested object for address
    'address': Ack.object(
      {
        'street': Ack.string,
        'city': Ack.string,
        'zipCode': Ack.string.matches(r'\d{5}', example: '12345'), // Simple 5-digit zip code regex
      },
      required: ['street', 'city'], // Street and city are required *within* the address object
    ),
  },
  // Specify required top-level properties for the *user* object
  required: ['name', 'email'],
);

Optional Fields

By default, fields defined in an Ack.object are optional unless included in the required list. You can also make any field explicitly nullable using .nullable(). See Making Schemas Nullable below.

final productSchema = Ack.object(
  {
    'id': Ack.string,         // Required
    'name': Ack.string,       // Required
    'description': Ack.string, // Optional (not in 'required' list)
    'price': Ack.double.min(0), // Required
    'tags': Ack.list(Ack.string).nullable(), // Optional list (can be null)
  },
  required: ['id', 'name', 'price'],
);

// Valid: description is missing, tags is null
final validData = {'id': 'p1', 'name': 'Thing', 'price': 9.99, 'tags': null};
print(productSchema.validate(validData).isOk); // true

// Invalid: name is missing
final invalidData = {'id': 'p2', 'price': 19.99};
print(productSchema.validate(invalidData).isOk); // false

Handling Nested Models

When working with nested objects or lists of objects, Ack handles the validation recursively. If you are using code generation or the SchemaModel class, accessing nested data is straightforward.

Accessing Nested Data:

Assuming you have validated data or a model instance (user):

// Direct access (if using generated models or SchemaModel)
// The type is automatically converted.
Address billingAddress = user.billingAddress;
print(billingAddress.city);

// Accessing items in a list of models
List<OrderItem> items = user.orderItems;
for (final item in items) {
  print(item.productName);
}

// If working directly with validated data (Map<String, dynamic>)
final validatedData = result.getOrThrow();
final addressMap = validatedData['address'] as Map<String, dynamic>?;
if (addressMap != null) {
  // You might need to validate/parse the nested part separately
  // if not using generated models.
  final nestedAddressSchema = Ack.object({...}); // Define address schema again
  final addressResult = nestedAddressSchema.validate(addressMap);
  if (addressResult.isOk) {
    final city = addressResult.getOrThrow()['city'];
    print(city);
  }
}

Updating Nested Data (Immutability):

When updating, it's best practice to create new instances rather than modifying existing ones, especially if using immutable models.

// Assume 'user' is an existing immutable model instance
final updatedUser = user.copyWith(
  // Create a new Address instance with the changed city
  address: user.address.copyWith(city: "Miami")
);

// If using generated schemas, you might convert back for re-validation
// final updatedSchema = UserSchema.fromModel(updatedUser);
// final validationResult = UserSchema.schema.validate(updatedSchema.toMap());

For more details on using generated schemas (like UserSchema.fromModel), see the Code Generation guide.

Allowing Additional Properties

By default, Ack.object ignores properties in the input data that are not defined in the schema. To capture these, use additionalProperties:

final flexibleSchema = Ack.object(
  {
    'id': Ack.int,
  },
  // Allow any other properties, as long as their values are strings
  additionalProperties: Ack.string,
);

final data = {'id': 1, 'name': 'Gadget', 'color': 'blue', 'status': 123};
final result = flexibleSchema.validate(data);

print(result.isOk); // false (because 'status' is not a string)

final validated = flexibleSchema.validate({'id': 1, 'name': 'Gadget'}).getOrThrow();
print(validated); // {id: 1, name: Gadget} - Extra properties are included

To disallow any extra properties, set additionalProperties: false:

final strictSchema = Ack.object(
  {
    'id': Ack.int,
    'name': Ack.string,
  },
  additionalProperties: false,
);

final dataWithExtra = {'id': 1, 'name': 'Test', 'extra': 'disallowed'};
final result = strictSchema.validate(dataWithExtra);

print(result.isOk); // false
print(result.getError()?.message); // Contains info about disallowed property 'extra'

Making Schemas Nullable

Any schema type can be made nullable by appending .nullable(). This means the schema will accept both its defined type and the value null.

// Nullable string
final middleNameSchema = Ack.string.nullable();

// Nullable date
final optionalDateSchema = Ack.string.isDate().nullable();

// Nullable object (the entire address object can be null)
final optionalAddressSchema = Ack.object(
  {
    'street': Ack.string,
    'city': Ack.string,
  },
  required: ['street', 'city'],
).nullable(); // Note .nullable() is outside Ack.object

// --- Validation Examples ---

// String checks
print(middleNameSchema.validate(null).isOk); // true
print(middleNameSchema.validate('Robert').isOk); // true

// Object checks
print(optionalAddressSchema.validate(null).isOk); // true (null object is allowed)
final validAddress = {'street': '1 Elm St', 'city': 'Gotham'};
print(optionalAddressSchema.validate(validAddress).isOk); // true (valid address object)

// Invalid nested object still fails even if object is nullable
final invalidAddress = {'street': '1 Elm St'}; // Missing required 'city'
print(optionalAddressSchema.validate(invalidAddress).isOk); // false