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 a schema constant (e.g.,userSchema).@AckType()goes on a schema variable/getter and produces an extension type for type-safe access (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.
Schema Generation from Classes with @AckModel()
Use @AckModel() when you have a Dart class that should drive schema generation. The generator creates a validation schema based on your class structure.
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({...});
You can use the generated schema for validation:
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,
);
}
For type-safe access without manual casting, define a schema variable in source and annotate it with @AckType() (see below).
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.
@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:
- Your original schema constants/getters remain in your source file.
- 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.enumValues,Ack.enumString)
Unsupported helpers include Ack.any, Ack.anyOf, and Ack.discriminated.
Use @AckModel for discriminated unions instead.
@AckType Resolution Requirements and Limitations
@AckType() now uses strict resolution for nested typed schemas. This prevents
silent fallbacks to Map<String, Object?> when references cannot be resolved
unambiguously.
- Nested object fields must reference a named top-level schema variable/getter. Anonymous inline object fields are not supported for typed generation.
Ack.list(...)elements must be statically resolvable. Dynamic expressions such asschemaFactory()or excessively deep method chains are rejected.- Cross-file references (direct import, prefixed import, re-export) are
supported, but prefixed references must resolve inside the specified prefix.
prefix.symbolnever falls back to a different namespace. - Object schema references used for typed wrappers must be annotated with
@AckType(). Unannotated object refs fail generation instead of degrading to raw map access. - Circular schema alias/reference chains fail generation with a clear circular reference error.
If you need a typed nested object, extract it to a top-level schema variable or
getter and annotate it with @AckType(), then reference that symbol.
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 | ❌ No | You have a class definition and want schema generation |
@AckType | Top-level schema variable/getter | ❌ No (uses existing) | ✅ Yes | You have a schema and want type-safe access |
Examples:
-
Use
@AckModelwhen you have a Dart class (or class hierarchy) that should drive schema generation. -
Use
@AckTypeon hand-written top-level schema variables or getters when you want type-safe extension type access. -
Note:
@AckTypeis not supported on classes or generated schemas. If you need extension types, define the schema directly in your source file.