Skip to content

Generator: resolve a field's type from the type's own schema, and retire @AckField(schema:) #185

Open

Description

@leoafarias

Problem

A class-first model can only use field types the generator already knows: JSON primitives, collections, @Schemable classes, and generated schema-first models. Any other type stops generation:

MetricDto.activate uses unsupported JamInvoke<Object?>; use a concrete type with a static class-first schema contract.

The only way through today is @AckField(schema: fn) on every field of that type, where fn is a top-level, zero-parameter function. Building this against #184's branch (b31d4b4) showed three costs:

  • Repetition. The annotation is written on each field, in every model that uses the type.
  • It replaces the whole field schema. On a List<T> field it does not apply per item, so a list needs its own schema function.
  • Generic types lose their type argument. @AckField(schema: invokeSchema<CompletionAction>) is accepted, but the generator emits invokeSchema() without the type argument (class_model_graph_builder.dart, _escapeHatchExpression, which uses only function.name). The analyzer reports nothing and parse fails at runtime. Today this forces one non-generic wrapper per type argument.

Proposal

Let a type say what its schema is, once, and have the generator resolve it wherever the type appears: as a field, as a list item, as a map value. Nothing is registered on the models that use it.

Types you own: a static schema on the type.

final class Slot {
  const Slot(this.id);
  final String id;

  static final schema = Ack.string().codec(decode: Slot.new, encode: (s) => s.id);
}

@Schemable()
final class SectionDto({
  required final Slot header,          // resolves to Slot.schema
  required final List<Slot> children,  // resolves per item
}) with _$SectionDtoAck;

This is the same name #173 gives schema-first models (Model.schema), so one convention covers generated and hand-written types. It should work for a class and for an extension type (the lookup in _namedSchemaForType currently accepts only a ClassElement).

Generic types: the same name, as a static generic method. A static field cannot use the class's type parameter, so:

final class Invoke<A extends Object?> {
  static AckSchema<String, Invoke<A>> schema<A extends Object?>() => …;
}

A field Invoke<CompletionAction> toggle resolves to Invoke.schema<CompletionAction>(), with the type arguments rendered from the field's type.

Types you do not own: one global registration. A static member cannot be added to DateTime, Uri or a Flutter Color. For these, register a schema once for the whole package, not per model. Candidates: generator options in build.yaml mapping a type to a schema function, or a library-level annotation. This is the part of the closed #93 (SchemaProvider<T> with @Schemable(useProviders: …)) worth keeping, without the per-model registration.

Deprecating @AckField(schema:)

With type-owned schemas, @AckField(schema:) has two remaining uses:

  1. A custom type. Replaced by the proposal above.
  2. A built-in type with a special shape, for example a String that must be a calendar date. This can be expressed as a constraint annotation, or as a small value type with its own schema (an extension type on String).

If case 2 holds up, @AckField(schema:) can be deprecated in 2.0 and removed later. @AckField(description:) and the other options stay.

Where it goes

_namedSchemaForType in packages/ack_generator/lib/src/analyzer/class_model_graph_builder.dart already resolves two kinds of named types (a @Schemable class through its facade, a generated schema-first model through $ack.schema) and returns null otherwise, which becomes the "unsupported" error at inference/schema_inference.dart (resolveNamed(type) ?? unsupported(type)). The new cases belong there, so they also apply inside collections.

Open questions

  • Should the generator require the static schema to be typed AckSchema<_, Self> and report a mismatch at generation time? Today a mismatched @AckField(schema:) only fails at runtime through a cast in the generated bridge.
  • What happens when a type has both a static schema and a global registration: reject, or prefer the type's own?
  • The form of the global registration: build.yaml options or a library-level annotation.
  • x-transformed: true is added to every codec schema and cannot be turned off. Should a type-owned schema be able to opt out?

Related

Activity

  1. added
    enhancementNew feature or request
    2.0Planned for Ack 2.0
    and removed
    2.0Planned for Ack 2.0
    on Oct 1, 2026
  2. leoafarias commented on Oct 1, 2026

    @leoafarias
    CollaboratorAuthor

    Moved out of 2.0: this is planned for the next 1.7 beta. It does not depend on the analyzer 13 move, which stays in 2.0 (#188).

  3. leoafarias commented on Oct 2, 2026

    @leoafarias
    CollaboratorAuthor

    Part of this landed in #190.

    What landed (class-first fields; also per List / Set item and as a Map value):

    • A static schema on a class, an extension type (declared implements Object), or an enum. An enum's own schema takes precedence over Ack.enumValues. Nothing is needed on the field, and generation checks that the schema produces the field's type.
    • A generic type's static schema<A>(), called with the field's type arguments as written, including void and Object? (Command.schema<void>()).
    • Passing the type arguments' schemas: when schema<A>() declares one positional AckSchema<Object, A> parameter per type parameter, the generator passes each argument's inferred schema (Box.schema<Row>(RowSchema.schema), Box.schema<List<Row>>(Ack.list(RowSchema.schema))). It checks the parameter count and each parameter's runtime type.
    • Schema-first objects can reference the same members, with the arguments written by hand.

    What remains:

    • A global registration for types you do not own (DateTime, Uri, a Flutter Color).
    • Deprecating @AckField(schema:).
    • Validating a schema parameter's boundary type argument and the method's type-parameter bounds at generation time. Today only the parameter's runtime type is checked, and the docs ask for AckSchema<Object, A>.
    • void type arguments in the schema-first direction. They are supported only in class-first fields today.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions