Ack Generator
Code generator for the Ack validation library that automatically creates schema validation code from annotated Dart classes.
Overview
Ack Generator analyzes your Dart models and produces corresponding Ack.object() schemas. You annotate your classes with @AckModel(), and the generator creates schema variables that you can use for runtime validation.
The generator handles:
- Basic schema generation from class fields
- Nested models and complex types
- Discriminated types for polymorphic validation
- Field-level constraints and customization
- Additional properties support
Installation
Add the following dependencies to your pubspec.yaml:
dependencies:
ack: ^1.0.0-beta.1
ack_annotations: ^1.0.0-beta.1
dev_dependencies:
ack_generator: ^1.0.0-beta.1
build_runner: ^2.4.0
Still on the 0.3 alpha line? Use
^0.3.0-alpha.0for all Ack packages until you migrate to1.0.0-beta.1.
Run dart pub get to install the packages.
Basic usage
1. Annotate your model
Create a Dart class and annotate it with @AckModel():
// user.dart
import 'package:ack_annotations/ack_annotations.dart';
part 'user.g.dart';
@AckModel()
class User {
final String name;
final String email;
final int? age;
User({required this.name, required this.email, this.age});
}
2. Generate the schema
Run the build_runner to generate the schema code:
dart run build_runner build
This creates a user.g.dart file containing the generated schema:
// user.g.dart (generated)
final userSchema = Ack.object({
'name': Ack.string(),
'email': Ack.string(),
'age': Ack.integer().optional(),
});
3. Use the generated schema
Import the generated part file and use the schema for validation:
import 'user.dart';
void main() {
final userData = {'name': 'Alice', 'email': 'alice@example.com', 'age': 30};
final result = userSchema.safeParse(userData);
if (result.isOk) {
final validatedData = result.getOrThrow();
final user = User(
name: validatedData['name'] as String,
email: validatedData['email'] as String,
age: validatedData['age'] as int?,
);
print('User created: ${user.name}');
} else {
print('Validation failed: ${result.getError()}');
}
}
Features
Automatic schema generation
The generator creates schemas based on your class fields and their types:
@AckModel()
class Product {
final String name;
final double price;
final bool inStock;
final List<String> tags;
Product({
required this.name,
required this.price,
required this.inStock,
required this.tags,
});
}
// Generated schema
final productSchema = Ack.object({
'name': Ack.string(),
'price': Ack.double(),
'inStock': Ack.boolean(),
'tags': Ack.list(Ack.string()),
});
Field constraints
Use @AckField to add validation constraints:
@AckModel()
class User {
@AckField(constraints: ['minLength(1)', 'maxLength(50)'])
final String name;
@AckField(constraints: ['email'])
final String email;
@AckField(constraints: ['min(0)', 'max(150)'])
final int? age;
User({required this.name, required this.email, this.age});
}
// Generated schema includes constraints
final userSchema = Ack.object({
'name': Ack.string().minLength(1).maxLength(50),
'email': Ack.string().email(),
'age': Ack.integer().min(0).max(150).optional(),
});
Custom JSON keys
Map class fields to different JSON property names:
@AckModel()
class User {
@AckField(jsonKey: 'full_name')
final String name;
@AckField(jsonKey: 'email_address')
final String email;
User({required this.name, required this.email});
}
// Generated schema uses custom keys
final userSchema = Ack.object({
'full_name': Ack.string(),
'email_address': Ack.string(),
});
Additional properties
Allow or disallow extra fields in validated objects:
@AckModel(additionalProperties: true)
class FlexibleModel {
final String id;
FlexibleModel({required this.id});
}
// Generated schema allows additional properties
final flexibleModelSchema = Ack.object({
'id': Ack.string(),
}, additionalProperties: true);
By default, additionalProperties is false, which means the schema rejects any fields not explicitly defined in the class.
Nested models
The generator handles nested model references:
@AckModel()
class Address {
final String street;
final String city;
Address({required this.street, required this.city});
}
@AckModel()
class User {
final String name;
final Address address;
User({required this.name, required this.address});
}
// Generated schemas
final addressSchema = Ack.object({
'street': Ack.string(),
'city': Ack.string(),
});
final userSchema = Ack.object({
'name': Ack.string(),
'address': addressSchema,
});
Advanced features
Discriminated types
Use discriminated types to validate polymorphic data structures. Define a base class with a discriminator key, then create subclasses with specific discriminator values:
@AckModel(discriminatedKey: 'type')
abstract class Shape {
String get type;
}
@AckModel(discriminatedValue: 'circle')
class Circle extends Shape {
@AckField(constraints: ['positive()'])
final double radius;
Circle({required this.radius});
@override
String get type => 'circle';
}
@AckModel(discriminatedValue: 'rectangle')
class Rectangle extends Shape {
@AckField(constraints: ['positive()'])
final double width;
@AckField(constraints: ['positive()'])
final double height;
Rectangle({required this.width, required this.height});
@override
String get type => 'rectangle';
}
The generator creates a discriminated schema that validates based on the discriminator field:
// Generated schemas
final circleSchema = Ack.object({
'type': Ack.literal('circle'),
'radius': Ack.double().positive(),
});
final rectangleSchema = Ack.object({
'type': Ack.literal('rectangle'),
'width': Ack.double().positive(),
'height': Ack.double().positive(),
});
final shapeSchema = Ack.discriminated(
discriminatorKey: 'type',
schemas: {
'circle': circleSchema,
'rectangle': rectangleSchema,
},
);
Use the discriminated schema to validate different shape types:
final circleData = {'type': 'circle', 'radius': 5.0};
final rectangleData = {'type': 'rectangle', 'width': 10.0, 'height': 20.0};
final circleResult = shapeSchema.safeParse(circleData);
final rectangleResult = shapeSchema.safeParse(rectangleData);
if (circleResult.isOk) {
final data = circleResult.getOrThrow();
final circle = Circle(radius: data['radius'] as double);
print('Circle with radius: ${circle.radius}');
}
if (rectangleResult.isOk) {
final data = rectangleResult.getOrThrow();
final rectangle = Rectangle(
width: data['width'] as double,
height: data['height'] as double,
);
print('Rectangle: ${rectangle.width} x ${rectangle.height}');
}
Supported constraints
You can use the following constraints with @AckField:
String constraints:
minLength(n)- Minimum string lengthmaxLength(n)- Maximum string lengthemail- Email format validationurl- URL format validationnotEmpty- Non-empty string
Number constraints:
min(n)- Minimum valuemax(n)- Maximum valuepositive()- Positive numbers onlynegative()- Negative numbers onlynonNegative()- Zero or positive numbersnonPositive()- Zero or negative numbers
List constraints:
minLength(n)- Minimum list lengthmaxLength(n)- Maximum list lengthnotEmpty- Non-empty list
final priceSchema = Ack.double().nonNegative().max(100);
Use nonNegative() / nonPositive() as concise aliases for .min(0) / .max(0) while keeping consistent error messages.
Usage examples
Validating API request data
@AckModel()
class CreateUserRequest {
@AckField(constraints: ['minLength(1)', 'maxLength(100)'])
final String username;
@AckField(constraints: ['email'])
final String email;
@AckField(constraints: ['minLength(8)'])
final String password;
CreateUserRequest({
required this.username,
required this.email,
required this.password,
});
}
// In your API handler
void handleCreateUser(Map<String, dynamic> requestBody) {
final result = createUserRequestSchema.safeParse(requestBody);
if (!result.isOk) {
return sendError(400, result.getError().toString());
}
final validatedData = result.getOrThrow();
final request = CreateUserRequest(
username: validatedData['username'] as String,
email: validatedData['email'] as String,
password: validatedData['password'] as String,
);
// Create user with validated data
createUser(request);
}
Validating configuration files
@AckModel()
class DatabaseConfig {
@AckField(constraints: ['minLength(1)'])
final String host;
@AckField(constraints: ['min(1)', 'max(65535)'])
final int port;
@AckField(constraints: ['minLength(1)'])
final String database;
final String? username;
final String? password;
DatabaseConfig({
required this.host,
required this.port,
required this.database,
this.username,
this.password,
});
}
// Load and validate configuration
void loadConfig(String jsonString) {
final json = jsonDecode(jsonString) as Map<String, dynamic>;
final result = databaseConfigSchema.safeParse(json);
if (!result.isOk) {
throw ConfigurationError('Invalid database config: ${result.getError()}');
}
final validatedData = result.getOrThrow();
final config = DatabaseConfig(
host: validatedData['host'] as String,
port: validatedData['port'] as int,
database: validatedData['database'] as String,
username: validatedData['username'] as String?,
password: validatedData['password'] as String?,
);
connectToDatabase(config);
}
Development
Regenerating code
If you modify your annotated models or add new constraints, regenerate the schemas:
# Clean previous builds
dart run build_runner clean
# Generate fresh code
dart run build_runner build
# Or use watch mode during development
dart run build_runner watch
Troubleshooting
Part directive missing: If you see errors about missing generated code, ensure your model file includes the part directive:
part 'your_file_name.g.dart';
Build conflicts: If the generator reports conflicts, run the build with the delete flag:
dart run build_runner build --delete-conflicting-outputs
Type resolution errors:
Ensure all referenced types have @AckModel() annotations or are built-in Dart types that Ack supports.
Contributing
Contributions are welcome. Follow these guidelines:
- Check existing issues before creating new ones
- Follow the existing code style and patterns
- Add tests for new features
- Update documentation for public API changes
- Run
melos testto ensure all tests pass
License
This project is licensed under the MIT License. See the LICENSE file for details.