ACK provides a fluent, unified schema-building solution for Dart and Flutter applications. It delivers clear constraints, descriptive error feedback, and powerful utilities for validating forms, AI-driven outputs, and JSON or CLI arguments.
- Validates diverse data types with customizable constraints
- Converts into OpenAPI Specs for LLM function calling and structured response support
- Offers a fluent API for intuitive schema building
- Provides detailed error reporting for validation failures
Add ACK to your pubspec.yaml
:
dart pub add ack
ACK provides schema types to validate different kinds of data. You can customize each schema with constraints, nullability, strict parsing, default values, and more using a fluent API.
Validates string data, with constraints like minimum length, maximum length, non-empty checks, regex patterns, and more.
Example:
import 'package:ack/ack.dart';
final schema = Ack.string
.minLength(5)
.maxLength(10)
.isNotEmpty()
.nullable(); // Accepts null
final result = schema.validate('hello');
if (result.isOk) {
print(result.getOrNull()); // "hello"
}
Validates integer data. Constraints include min/max values, exclusive bounds, and multiples.
Example:
final schema = Ack.int
.minValue(0)
.maxValue(100)
.multipleOf(5);
final result = schema.validate(25);
Similar to Integer Schema, but for doubles:
final schema = Ack.double
.minValue(0.0)
.maxValue(100.0)
.multipleOf(0.5);
final result = schema.validate(25.5);
Validates boolean data:
Example:
final schema = Ack.boolean.nullable();
final result = schema.validate(true);
This schema accepts boolean values or null.
Validates lists of items, each item validated by an inner schema:
Example:
final itemSchema = Ack.string.minLength(3);
final listSchema = Ack.list(itemSchema)
.minItems(2)
.uniqueItems();
final result = listSchema.validate(['abc', 'def']);
Validates Map<String, Object?>
with property definitions and constraints on required fields, additional properties, etc.
Example:
final schema = Ack.object(
{
'name': Ack.string.minLength(3),
'age': Ack.int.minValue(0).nullable(),
},
required: ['name'],
);
final result = schema.validate({'name': 'John'});
This schema requires a "name" property (string, min length 3) and allows an optional "age" property (integer >= 0), with at least one property.
For scalar schemas (String, Integer, Double, Boolean), ACK can parse strings or numbers into the correct type if strict is false (the default). If you set strict, the schema only accepts an already-correct type.
// By default, Ack.int will accept "123" and parse it to 123.
final looseSchema = Ack.int;
print(looseSchema.validate("123").isOk); // true
// If you require strictly typed ints (no string parsing):
final strictSchema = Ack.int.strict();
print(strictSchema.validate("123").isOk); // false
print(strictSchema.validate(123).isOk); // true
You can set a default value so that if validation fails or if the user provides null, the schema returns the default:
// Setting default value in the constructor:
final schema = Ack.string(
defaultValue: 'Guest',
nullable: true,
).minLength(3);
// This fails the minLength check, but returns the default "Guest"
final result = schema.validate('hi');
print(result.getOrNull()); // "Guest"
final nullResult = schema.validate(null);
print(nullResult.getOrNull()); // "Guest"
Important: If the parsed value is invalid or null, but a default value is present, ACK will return Ok(defaultValue) instead of failing.
You can extend ConstraintValidator<T>
or OpenApiConstraintValidator<T>
to create your own validation rules. For example:
class OnlyFooStringValidator extends OpenApiConstraintValidator<String> {
const OnlyFooStringValidator();
@override
String get name => 'only_foo';
@override
String get description => 'String must be "foo" only';
@override
bool isValid(String value) => value == 'foo';
@override
ConstraintError onError(String value) {
return buildError(
message: 'Value "$value" is not "foo".',
context: {'value': value},
);
}
// If you want this constraint to appear in OpenAPI:
@override
Map<String, Object?> toSchema() {
// Typically you'd put `"enum": ["foo"]`, or similar
return {
'enum': ['foo'],
'description': 'Must be exactly "foo"',
};
}
}
// Using it:
final schema = Ack.string.withConstraints([OnlyFooStringValidator()]);
final result = schema.validate("bar"); // Fails validation
When you implement OpenApiConstraintValidator<T>
, your custom validator's toSchema()
output is automatically merged into the final JSON schema. This means you can add fields like pattern
, enum
, minimum
, etc., as recognized by OpenAPI or JSON Schema.
// Example usage with the built-in OpenApiSchemaConverter
final converter = OpenApiSchemaConverter(schema: schema);
print(converter.toJson());
The library merges all constraints' toSchema()
results, so you get a single cohesive OpenAPI spec for your entire schema.
ACK's fluent API lets you chain methods:
final schema = Ack.int
.minValue(0)
.maxValue(100)
.multipleOf(5)
.nullable() // accept null
.strict(); // require actual int type
Calling schema.validate(value)
returns a SchemaResult<T>
, which can be:
Ok<T>
: Access the validated value withgetOrNull()
orgetOrThrow()
.Fail<T>
: ContainsList<SchemaError>
with messages describing which constraints failed.
final result = schema.validate(120);
if (result.isOk) {
print("Valid: ${result.getOrNull()}");
} else {
print("Errors: ${result.getErrors()}");
}
// You can also use validateOrThrow:
try {
schema.validateOrThrow(120);
} catch (e) {
print(e); // AckException with details
}
ACK can generate OpenAPI schema definitions from your schemas, aiding in API documentation or code generation.
final schema = Ack.object({
'name': Ack.string
.minLength(2)
.maxLength(50),
'age': Ack.int
.minValue(0)
.maxValue(120),
}, required: ['name', 'age']);
final converter = OpenApiSchemaConverter(schema: schema);
final openApiSchema = converter.toSchema();
print(openApiSchema);
/* Returns schema like:
{
"type": "object",
"required": ["name", "age"],
"properties": {
"name": {
"type": "string",
"minLength": 2,
"maxLength": 50
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 120
}
}
}
*/
Tip
When using LLMs with limited OpenAPI support, ACK lets you add schema instructions directly into prompts while keeping JSON response validation. Some LLM providers let you ensure the response is valid JSON even without a schema. This might work better for you.
final schema = Ack.object(
{
'name': Ack.string.minLength(2).maxLength(50),
'age': Ack.int.minValue(0).maxValue(120),
},
required: ['name', 'age'],
);
final converter = OpenApiSchemaConverter(schema: schema);
// Build a prompt for the LLM that includes the schema
final prompt = '''
You are a helpful assistant. Please provide information about a person following this schema:
${converter.toResponsePrompt()}
''';
/* Will output:
<schema>
{
"type": "object",
"required": ["name", "age"],
"properties": {
"name": {
"type": "string",
"minLength": 2,
"maxLength": 50
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 120
}
},
"additionalProperties": false
}
</schema>
Your response should be valid JSON, that follows the <schema> and formatted as follows:
<response>
{valid_json_response}
</response>
<stop_response>
*/
// Simulated LLM response
final llmResponse = '''
Here is the person's information:
<response>
{
"name": "John Smith",
"age": 35
}
</response>
''';
final jsonPayload = converter.parseResponse(llmResponse);
print(jsonPayload);
Every call to .validate(value)
returns a SchemaResult<T>
object, which is either Ok<T>
or Fail<T>
:
Ok
: Access the data viagetOrNull()
orgetOrThrow()
Fail
: InspectgetErrors()
for a list ofSchemaError
describing the failures
- Fluent Methods:
nullable()
strict()
withConstraints([ ... ])
validate(value)
→SchemaResult<T>
validateOrThrow(value)
→ throwsAckException
on errors
- Default Values: Provide
defaultValue: T?
directly in the schema constructor or via.call(defaultValue: X)
. - Custom Constraints: Extend
ConstraintValidator<T>
orOpenApiConstraintValidator<T>
to add your own logic. - OpenAPI: Use
OpenApiSchemaConverter(schema: yourSchema).toSchema()
(or.toJson()
) to generate specs.
Happy validating with ACK!