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., userSchemaUserType).
  • Both annotations live in package:ack_annotations and are processed by the ack_generator builder via dart 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, add @AckType() to the generated schema variable (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.
  • 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/safeParse so you can do final 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 a SchemaResult<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

AnnotationTargetGenerates Schema?Generates Extension Type?Use When
@AckModelDart class Yes NoYou have a class definition and want schema generation
@AckTypeSchema variable No (uses existing) YesYou have a schema and want type-safe access

Examples:

  • Use @AckModel when you have a Dart class (or class hierarchy) that should drive schema generation.

  • Use @AckType on schema variables (either hand-written or generated by @AckModel) when you want type-safe extension type access.

  • Combine both: Use @AckModel to generate the schema from your class, then use @AckType on the generated schema variable if you want extension type wrappers.

Build Runner Checklist

  1. Add ack_generator and ack_annotations to pubspec.yaml.
  2. Include a build.yaml or rely on defaults.
  3. Ensure source files have a part '<file>.g.dart';.
  4. Run dart run build_runner build (or watch) after changing schemas.