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) Ack.discriminated(...)(with constraints below)
Unsupported helpers include Ack.any and Ack.anyOf.
Ack.discriminated(...) with @AckType (Current constraints)
schemasmust be a non-empty map literal.- Base schema cannot be nullable.
- Each branch must be a top-level schema variable/getter reference.
- Each branch must be
@AckType, object-shaped, non-nullable, and declared in the same library. - Branch discriminator literal (
Ack.literal(...)) must match the branch key inschemas. - A branch schema can belong to only one discriminated base.
@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>.
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.