Repository navigation
Generator: resolve a field's type from the type's own schema, and retire @AckField(schema:) #185
Description
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 emitsinvokeSchema()without the type argument (class_model_graph_builder.dart,_escapeHatchExpression, which uses onlyfunction.name). The analyzer reports nothing andparsefails 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:
- A custom type. Replaced by the proposal above.
- A built-in type with a special shape, for example a
Stringthat must be a calendar date. This can be expressed as a constraint annotation, or as a small value type with its ownschema(an extension type onString).
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
schemato be typedAckSchema<_, 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
schemaand a global registration: reject, or prefer the type's own? - The form of the global registration:
build.yamloptions or a library-level annotation. x-transformed: trueis added to every codec schema and cannot be turned off. Should a type-owned schema be able to opt out?
Related
- Generator: add a
Model.schemashorthand forModel.$ack.modelSchema#173:Model.schemashorthand for schema-first models. - feat(ack)!: unify annotations and support primary constructors #184:
@Schemablewith primary constructors, the branch this was tested against. - Add constructor-driven @Schemable and typed SchemaProvider<T> #93 (closed):
SchemaProvider<T>with per-modeluseProviders.
Activity
leoafarias commentedon Oct 1, 2026
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).
feat(ack_generator): accept void as a type argument of type-owned sch…
feat(ack_generator): pass type argument schemas to a generic static s…
leoafarias commentedon Oct 2, 2026
Part of this landed in #190.
What landed (class-first fields; also per
List/Setitem and as aMapvalue):schemaon a class, an extension type (declaredimplements Object), or an enum. An enum's ownschematakes precedence overAck.enumValues. Nothing is needed on the field, and generation checks that the schema produces the field's type.schema<A>(), called with the field's type arguments as written, includingvoidandObject?(Command.schema<void>()).schema<A>()declares one positionalAckSchema<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.What remains:
DateTime,Uri, a FlutterColor).@AckField(schema:).AckSchema<Object, A>.voidtype arguments in the schema-first direction. They are supported only in class-first fields today.feat(ack,ack_generator): enforce type-owned schema bounds/boundaries …