TypeSafe Schemas
Ack generates extension types that wrap validated data so you can work with
strongly typed objects instead of raw Map<String, Object?>. This guide shows
how @AckModel() and @AckType() annotations interact with the generator to
produce these typed views.
Overview
@AckModel()goes on a Dart class and produces both a schema constant (e.g.,userSchema) and an extension type named<ClassName>Type.@AckType()goes on a schema variable/getter and produces only an extension type (the schema already exists). The type name is derived from the variable name (e.g.,userSchema→UserType).- Both annotations live in
package:ack_annotationsand are processed by theack_generatorbuilder viadart run build_runner build.
Typed Schemas from Classes with @AckModel()
Use @AckModel() when you have a Dart class that should drive schema generation. The generator creates both the validation schema and a typed wrapper.
import 'package:ack_annotations/ack_annotations.dart';
part 'user.g.dart';
@AckModel(description: 'Profile details')
class User {
final String name;
final int age;
User({required this.name, required this.age});
}
Running dart run build_runner build writes user.g.dart with:
- Schema constant:
final userSchema = Ack.object({...}); - Extension type:
extension type UserType(Map<String, Object?> _data)that exposes typed getters,parse,safeParse,toJson,copyWith, and equality.
You can use either the schema directly or the generated extension type:
// Option 1: Use the schema directly (returns Map<String, Object?>)
final result = userSchema.safeParse(json);
if (result.isOk) {
final data = result.getOrThrow();
final user = User(
name: data['name'] as String,
age: data['age'] as int,
);
}
// Option 2: Use the extension type (returns UserType)
final result = UserType.safeParse(json);
if (result.isOk) {
final user = result.getOrThrow();
print(user.name); // -> String (type-safe!)
print(user.age); // -> int (type-safe!)
}
Discriminated Hierarchies
Annotate an abstract base with discriminatedKey and each subtype with a
discriminatedValue to receive:
- A generated discriminated schema that maps discriminator values to subtype schemas.
- A sealed class plus subtype extension types so you can switch exhaustively on the parsed result.
@AckModel(discriminatedKey: 'type')
abstract class Shape {
String get type;
}
@AckModel(discriminatedValue: 'circle')
class Circle extends Shape {
@override
String get type => 'circle';
final double radius;
Circle(this.radius);
}
Typed Schemas from Hand-Written Definitions with @AckType()
Use @AckType() when you write schemas manually but still want type-safe access via extension types. Unlike @AckModel, this annotation does not generate a schema—it only generates an extension type wrapper for an existing schema.
import 'package:ack/ack.dart';
import 'package:ack_annotations/ack_annotations.dart';
part 'user_schema.g.dart';
@AckType()
final userSchema = Ack.object({
'id': Ack.string(),
'email': Ack.string().email().nullable(),
'address': addressSchema,
});
@AckType()
final addressSchema = Ack.object({
'street': Ack.string(),
'city': Ack.string(),
});
After running dart run build_runner build, the part file contains:
- The original schema constants remain unchanged (so existing imports keep working).
- Extension type
UserType(Map<String, Object?> _data)with typed getters (removes "Schema" suffix from variable name, adds "Type"). - Extension type
AddressType(Map<String, Object?> _data)for the nested schema. - Static helpers
parse/safeParseso you can dofinal user = UserType.parse(json);.
Key difference from @AckModel: The schemas (userSchema, addressSchema) already exist in your source code. The annotation only adds the extension type layer.
Supported Schema Shapes
@AckType() works with:
Ack.object(...)- Primitive schemas (
Ack.string,Ack.integer,Ack.double,Ack.boolean) - Lists of supported schemas (
Ack.list(...)) - Literal/enum helpers (
Ack.literal,Ack.enumString,Ack.enumValues)
Unsupported helpers include Ack.any, Ack.anyOf, and Ack.discriminated.
Use @AckModel for discriminated unions instead.
Working with Extension Types
TypeName.parse(Object? data)throws on invalid input.TypeName.safeParse(Object? data)returns aSchemaResult<TypeName>.type.copyWith(...)produces modified copies without mutating the backing map.type.toJson()returns the underlying map or scalar value.
Extension types implement the underlying Dart type, so primitive wrappers still
behave like String, int, etc., while object wrappers implement
Map<String, Object?>.
Choosing Between @AckModel and @AckType
| Annotation | Target | Generates Schema? | Generates Extension Type? | Use When |
|---|---|---|---|---|
@AckModel | Dart class | ✅ Yes | ✅ Yes | You have a class definition and want both validation and typed access |
@AckType | Schema variable | ❌ No (uses existing) | ✅ Yes | You already wrote the schema manually and just want typed access |
Examples:
-
Use
@AckModelwhen you have a Dart class (or class hierarchy) that should drive schema generation. The generator creates both the schema and the extension type. -
Use
@AckTypefor hand-written schemas, shared schema fragments, or when you need typed access to a structure that doesn't have a corresponding Dart class. -
Mix both: You can reference a
@AckTypeschema inside an@AckModelclass field, or use@AckModelclasses within@AckTypeschema definitions.