The rewrite generator (graphitron-rewrite) is directive-compatible with the legacy generator (graphitron-codegen-parent): the same @table, @field, @service, @reference, @asConnection, @nodeId, @condition directives all do approximately the same thing on the rewrite, and a schema authored against the legacy generator’s directive surface will mostly compile under the rewrite without changes. The "mostly" is what this recipe covers: the hard removals (build fails until you remove the directive), the synthesis shims (build succeeds with a WARN today, will fail later), the deprecated arguments (build succeeds with a deprecation notice from graphql-java), and the one behavior divergence the rewrite intentionally preserves over legacy semantics.
This recipe is organised by what changes for you, not by which directive each topic touches. Run through the sections in order; each lands a class of migration steps before the next one introduces a new shape.
Authoritative supported surface
The rewrite’s directive surface and the schema shapes its classifier emits are both regenerated from the test suite. The two lists below are mechanically derived: every directive listed under "Supported directives" is declared in the rewrite’s directives.graphqls, and every shape under "Supported schema shapes" has at least one execution-tier or pipeline-tier fixture that exercises it through end-to-end classification.
A schema using only the directives and shapes named below should migrate cleanly. The remaining sections of this recipe cover the mechanical rewrites the legacy → rewrite migration requires.
Supported directives
The rewrite generator declares the following directives. Each is documented in the architecture chapter and exercised by at least one execution-tier or pipeline-tier test fixture:
-
@asConnection -
@sourceRow -
@condition -
@defaultOrder -
@discriminate -
@discriminator -
@enum -
@error -
@experimental_constructType -
@externalField -
@field -
@index -
@lookupKey -
@multitableReference -
@mutation -
@node -
@nodeId -
@notGenerated -
@order -
@orderBy -
@record -
@reference -
@service -
@splitQuery -
@table -
@tableMethod
Legacy-only directives
These directives existed in legacy graphitron but are removed in the rewrite. Drop them from your schema (or replace per the notes below) before migrating:
-
@experimental_procedureCall
Argument-shape changes
The following directives have argument-shape changes between legacy and rewrite. Consumers must update existing usages:
@externalField
-
+ reference: ExternalCodeReference! (rewrite only)
@field
-
- javaName: String (legacy only)
Supported schema shapes are enumerated below by output sealed-leaf class. Each supported entry has at least one execution-tier or pipeline-tier fixture in the rewrite test suite.
GraphitronField
-
UnclassifiedField: A field that could not be classified. A schema containing unclassified fields is invalid —
RootField
-
MutationDeleteTableField
-
MutationInsertTableField
-
MutationServiceRecordField: A mutation field backed by a developer-provided service method, returning a non-table type
-
MutationServiceTableField: A mutation field backed by a developer-provided service method, returning a table-mapped type
-
MutationUpdateTableField
-
MutationUpsertTableField
-
QueryInterfaceField: A root query field returning a multi-table
GraphitronType.InterfaceType -
QueryLookupTableField
-
QueryNodeField
-
QueryNodesField
-
QueryServiceRecordField: A root query field backed by a developer-provided service method, returning a non-table type
-
QueryServiceTableField: A root query field backed by a developer-provided service method, returning a table-mapped type
-
QueryTableField
-
QueryTableInterfaceField
-
QueryTableMethodTableField: A root query field using
@tableMethod. The developer provides a pre-filtered -
QueryUnionField: A root query field returning a multi-table
GraphitronType.UnionType
ChildField
-
ColumnField: A single-column output carrier on a table-backed parent. The column’s value reaches the
-
ColumnReferenceField: A single-column output carrier on a table-backed parent reached through a
@reference -
CompositeColumnField: Composite-key output carrier on a table-backed parent. Carries
columnsof arity -
(not yet supported) CompositeColumnReferenceField: Composite-key output carrier on a table-backed parent reached through a
@reference -
ComputedField: A child field using
@externalField— the developer provides a static method -
ConstructorField
-
ErrorsField: The
errorsfield on a payload type. Lift target for the payload-side of a -
InterfaceField: A child field on a
GraphitronType.TableBackedTypeparent returning a multi-table -
LookupTableField
-
NestingField
-
ParticipantColumnReferenceField: A scalar field on a
GraphitronType.TableInterfaceTypeparticipant that -
PassthroughDataField: A passthrough payload’s data field — the unique
@table-element field on -
PropertyField
-
RecordField
-
RecordLookupTableField
-
RecordTableField
-
ServiceRecordField: A child field backed by a developer-provided service method (
@service), where the -
ServiceTableField: A child field backed by a developer-provided service method (
@service), where the -
SplitLookupTableField
-
SplitTableField
-
TableField
-
TableInterfaceField
-
TableMethodField: A child field using
@tableMethod— the developer provides a pre-filtered -
UnionField: A child field on a
GraphitronType.TableBackedTypeparent returning a multi-table
InputField
-
ColumnField: A field in a
@table-annotated input type, successfully resolved to a SQL column -
ColumnReferenceField: A field in a
@table-annotated input type that uses@referenceto reach a -
CompositeColumnField: Composite-key input carrier on a
@table-annotated input type. Carries -
CompositeColumnReferenceField: Composite-key input reference carrier on a
@table-annotated input type whose -
NestingField: A field in a
@table-annotated input type whose GraphQL type is itself an input
GraphitronType
-
(not yet supported) ConnectionType: A Relay connection object type — the outer wrapper whose fields are
-
(not yet supported) EdgeType: A Relay edge object type — the inner wrapper whose fields are
cursor: String!and -
EnumType: A GraphQL enum type. Classifier records it so
schema.types()is complete for -
ErrorType: An object type annotated with
@error. Maps Java exceptions to GraphQL error responses -
InterfaceType: An interface with no directives. Participating types may be table-bound (
ParticipantRef.TableBound) -
JavaRecordInputType: A non-table input type backed by a Java
recordclass -
JavaRecordType: A
@recordtype backed by a Javarecordclass -
(not yet supported) JooqRecordInputType: A non-table input type backed by a jOOQ
Record<?>(not table-bound) -
(not yet supported) JooqRecordType: A
@recordtype backed by a jOOQRecord<?>(not table-bound) -
JooqTableRecordInputType: A non-table input type backed by a jOOQ
TableRecord<?> -
JooqTableRecordType: A
@recordtype backed by a jOOQTableRecord<?> -
NodeType: A GraphQL object type annotated with both
@tableand@node -
(not yet supported) PageInfoType: The Relay
PageInfoobject type. Exactly one instance per schema; the classifier -
PlainObjectType: A plain SDL object type — no
@table,@record,@error, or other -
PojoInputType: A non-table input type backed by a plain Java class (POJO), or one whose backing class
-
PojoResultType: A
@recordtype backed by a plain Java class (POJO), or one whose backing class -
RootType: A root operation type (Query or Mutation). Unmapped — no source context, no SQL until
-
TableInputType: A GraphQL input object type annotated with
@table -
TableInterfaceType: An interface annotated with
@tableand@discriminate, where implementing -
TableType: A GraphQL object type annotated with
@table, without@node -
UnclassifiedType: A type that could not be classified — examples include an unresolvable
@table -
UnionType: A union type whose member types all have
@table
Hard removals
Two directives the rewrite refuses to accept. Both stay declared so the SDL still parses, but every application is rejected at classify time with a "no longer supported" message.
@notGenerated
Legacy schemas used it to mark fields the developer would wire by hand through RuntimeWiring; the rewrite requires every field to be fully described by the schema. Any application is rejected at build time:
@notGenerated is no longer supported. Remove the directive; fields must be fully described by the schema.
The directive is still parsed (so the SDL still validates), but every classify-time application fails with that message. The migration is a one-step delete-and-replace: remove the @notGenerated and add the directive that names the actual data source.
| Original site | Rewrite replacement | Notes |
|---|---|---|
|
Wire the resolver as a service method; the rewrite generates the data fetcher and dispatches to your code. See How-to: Handle services for the argument binding and return-shape rules. |
|
|
|
|
|
|
The |
|
|
Service methods can be batched with |
|
(no replacement needed) |
The legacy "skip interface/union wiring" axis was always tied to |
The @notGenerated reference page carries the same matrix; this recipe expands the right-hand column with cross-links to the recipe each replacement is best documented in.
@multitableReference
Legacy schemas used @multitableReference(routes: […]) to declare per-implementation join paths for a field returning a multi-table interface or union. The rewrite generates multi-table interface dispatch from @discriminate / @discriminator (with @reference for the FK-driven join) and does not need a separate per-route description. Any application is rejected at build time:
@multitableReference is no longer supported. Remove the directive; the rewrite generates multi-table interface dispatch from @discriminate / @discriminator without an explicit multitable-reference path.
Migration is a one-step delete-and-rebuild: drop the @multitableReference directive (and its ReferencesForType argument tree) and model the multi-table dispatch through @discriminate / @discriminator. How-to: Polymorphic types covers the supported shapes.
Synthesis shims: WARN today, error later
Two paths still work in the rewrite for backwards compatibility but emit a per-occurrence WARN at build time. Plan to migrate them before the shim retirement gate fires (no committed date; tracked at graphitron-rewrite/roadmap/retire-synthesis-shims.md).
Bare id: ID! on @node types
Legacy schemas frequently relied on the generator inferring an opaque-ID carrier from a bare ID! field on a @node type. The rewrite still does this, but warns:
# Legacy form, still works, emits a WARN
type Customer implements Node @table(name: "customer") @node {
id: ID!
customerId: Int! @field(name: "CUSTOMER_ID")
}
Migration: declare @nodeId explicitly on the carrier:
# Canonical form, no WARN
type Customer implements Node @table(name: "customer") @node {
id: ID! @nodeId
customerId: Int! @field(name: "CUSTOMER_ID")
}
The WARN message names the field: field 'Customer.id' synthesizes an `@nodeId carrier without the directive; declare @nodeId explicitly.` It mentions the roadmap path so the team-internal trace from log line to migration ticket is one step.
When the shim retires, the WARN flips to a classify-time rejection: the field reads as UnclassifiedField and the build fails with a "column 'id' could not be resolved" diagnostic.
[ID!] @field(name: "X_ID") on @table input types
Legacy @table input types let an ID! (or [ID!]) field carry a column name through @field(name:) and the FK qualifier was inferred from the catalog. The rewrite still does this, but warns. Migration: replace the @field(name:) annotation with an explicit @nodeId(typeName:).
Before:
# Legacy form, still works, emits a WARN
input CustomerFilter @table(name: "customer") {
customerIds: [ID!] @field(name: "CUSTOMER_ID")
}
After:
# Canonical form
input CustomerFilter @table(name: "customer") {
customerIds: [ID!] @nodeId(typeName: "Customer")
}
When the FK is ambiguous (more than one path from the input’s @table to the target), add an explicit @reference(path: [{key: "fk_constraint_name"}]) alongside the @nodeId. The @reference recipe covers the FK-disambiguation mechanics; the migration is otherwise mechanical.
@table no longer auto-promotes to NodeType
Historical only: legacy versions of the rewrite would silently promote any @table SDL type to NodeType if its backing jOOQ class carried NODE_TYPE_ID / NODE_KEY_COLUMNS metadata, even without implements Node @node. This behavior was retired (the synthesis shim plan documents the trigger: a mass typeId collision between ~200 backing types each carrying __NODE_TYPE_ID = "195" produced a symmetric demotion to UnclassifiedType).
The current rule: a @table type without implements Node @node is a regular TableType, regardless of catalog metadata. If a schema relied on the auto-promotion (the type was reachable via Query.node or carried @nodeId fields), add the explicit implements Node @node declaration. The fields inside the type that do depend on node behavior (the @nodeId synthesis shim from the previous section) will still warn until they’re explicit.
Deprecated arguments: phase out at your leisure
The directives below still accept these arguments, but graphql-java surfaces a deprecation notice every time the schema is parsed, and the long-term plan is to drop them. Migrate when you next touch the directive.
connectionName: on @asConnection
# Legacy override, deprecated
movies: [Movie!]! @asConnection(connectionName: "SharedMoviesConnection")
otherMovies: [Movie!]! @asConnection(connectionName: "SharedMoviesConnection")
The override coalesces multiple connection-bearing fields onto one synthesised type. The rewrite default produces one Connection type per field (<ParentType><FieldName>Connection); sharing a name across fields conflates distinct semantic contexts (different parents, filters, orders, cursor formats) at the type level.
Migration: drop the argument and let each field own its synthesised connection type. The four reasons for the per-field default and the migration considerations live in How-to: Cursor-paginated connections under "One field, one connection type".
name: on ExternalCodeReference
# Legacy form, deprecated
@service(service: {name: "SampleQueryService", method: "filmsByService"})
Replaced with className::
# Canonical form
@service(service: {className: "no.sikt.graphitron.rewrite.test.services.SampleQueryService", method: "filmsByService"})
className: is the FQCN; the legacy name: was a short-name resolved through the plugin’s externalReferences: config. Both still work, but only className: is the canonical form going forward. The migration is mechanical: copy the FQCN from the legacy externalReferences: entry into className: and drop the entry.
@index on enum values
# Legacy form, deprecated
enum FilmSort {
BY_TITLE @index(name: "IDX_TITLE")
}
Replaced with @order(index:):
# Canonical form
enum FilmSort {
BY_TITLE @order(index: "IDX_TITLE")
}
@order is the unified enum-value sort surface; it accepts index:, fields:, or primaryKey: (exactly one). How-to: Sort results covers the column-source choice. @index still works on enum values today; the deprecation notice at parse time is the migration prompt.
Behavior divergence: @condition cascade
The rewrite intentionally diverges from the legacy generator on one semantic point. Schemas that relied on the legacy reading need to be updated; schemas that didn’t (or that already wrote schemas the rewrite reads correctly) are unaffected.
Legacy reading. @condition(override: true) on an outer field meant "outer owns everything": all inner explicit @condition annotations on the input’s fields were dropped, and the outer method’s predicates ran alone.
Rewrite reading. @condition(override: true) suppresses only the implicit column = ? predicates the rewrite would have emitted on its own. Inner explicit @condition annotations still fire: every @condition you write produces SQL.
The rewrite’s reading is the intended one. Under legacy semantics, an inner @condition was meaningless once any ancestor used override: true, which made schema review brittle (an outer override silently inert-ified inner annotations). The rewrite’s reading is composable: each @condition always fires; only the rewrite’s own implicit predicates are negotiable.
Schemas that relied on the legacy "outer owns everything" silently dropping inner conditions will produce different SQL under the rewrite. The migration is to rewrite the schema so the inner @condition annotations describe what should fire (instead of relying on them being ignored), or to remove them if their absence is the intent.
The example schema’s filmsOuterOverrideTableInput and filmsOuterOverridePlainInput fixtures pin the rewrite’s behavior as a regression-fence. The full mechanics live in How-to: Stacking and overriding conditions under "Divergence from legacy semantics".
Things that didn’t change
Every other directive on the legacy surface reads under the rewrite the same way, with one nuance per directive worth flagging:
-
@table,@field,@reference: unchanged; the catalog-driven mechanics are identical. -
@service,@tableMethod,@externalField,@enum,@record: unchanged surface; the only change is thename:→className:migration onExternalCodeReference(above). -
@discriminate/@discriminator: unchanged; the rewrite generates the type resolvers and runtime dispatchers automatically (noRuntimeWiring-side wiring needed). -
@asConnection: unchanged synth-vs-declared shape; onlyconnectionName:is deprecated. The rewrite emits the same Relay connection contract. -
@orderBy,@order,@defaultOrder: unchanged; the rewrite recognises the same shapes. The cursor wire format is the same base64-encoded NUL-separated tuple. -
@node,@nodeId: unchanged contract; the synthesis shims (above) are the only migration work.keyColumns:migration semantics carry over (cursor invalidation on key changes). -
@condition: same surface; the cascade divergence (above) is the only behavior change. -
@error: same handler-tuple surface; the rewrite’s classify-time rejection of duplicate tuples is new. How-to: The errors channel covers the dispatch loop and what’s currently a no-op (description:is captured but unused). -
@lookupKey,@splitQuery: unchanged. Mid-migration schemas that mixed@splitQuerywith@notGenerated(the legacy custom-batch-fetcher pattern) need to migrate the body to@service(above).
Migration sequence
The recommended order, from least invasive to most:
-
Run a
mvn installto surface every@notGeneratedrejection. Replace each with the directive named in the matrix above. The build won’t compile under the rewrite until this lands. -
Audit the build log for
synthesizes an `@nodeIdcarrier without the directive` WARNs; declare@nodeIdexplicitly on each. -
Audit input types for
[ID!] @field(name: "X_ID")patterns; replace with[ID!] @nodeId(typeName: "T"). -
Drop
connectionName:arguments from@asConnectionand let each field own its connection type. -
Migrate
name:toclassName:onExternalCodeReferenceinstances. Drop the corresponding entries fromexternalReferences:plugin config. -
Migrate
@indexto@order(index:)on enum values. -
If your schema uses
@condition(override: true)with nested inputs that carry their own@condition, audit whether the legacy "outer owns everything" semantics were load-bearing. If they were, the SQL will change; rewrite the inner annotations to express the intended predicate set explicitly.
Steps 1 is non-optional (the build fails). Steps 2-3 will become non-optional when the synthesis shims retire. Steps 4-6 produce deprecation notices but no failures. Step 7 only matters for schemas that relied on the divergence; most schemas don’t.
Pitfalls
-
@notGeneratedis parsed but always rejected. Removing the directive is the only fix; there is no warn-only mode and no migration shim. Schemas that still carry it will not compile. -
Synthesis shims run silently in IDE / language server contexts. The WARN is a build-time log line; an IDE that loads the schema for completion may not surface it. Periodically grep your build logs for
synthesizes anto catch shim-bearing fields. -
@tableno longer auto-promotes toNodeType. If a previously-promoted type breaks under the rewrite (typically:Query.nodereturnsnullfor what used to resolve, or@nodeIdsynthesis stops firing), addimplements Node @nodeto opt in explicitly. -
Deprecated arguments still work. The deprecation notice from graphql-java is informational, not blocking. The migration is your call until the deprecation flips to removal in a future release.
-
The
@conditioncascade divergence is a real SQL change. Schemas that relied on legacy "outer owns everything" semantics produce different result sets under the rewrite, not different errors. Auditoverride: truesites with nested-input@conditionannotations before promoting the rewrite to production. -
The rewrite is directive-compatible, not field-classifier-compatible. A schema that compiled under legacy may surface new diagnostics under the rewrite (stricter classification for shapes the legacy generator silently accepted). The diagnostic always names the field; treat each as the rewrite’s stricter-shape analysis catching what legacy would have produced as a runtime error.
-
Plugin configuration migrates separately. The schema-side migration is what this recipe covers;
<schemaInputs>,<outputDirectory>,<outputPackage>,<jooqPackage>, and<namedReferences>on the rewrite Mojo do not have a 1:1 mapping with every legacy plugin parameter. The legacy<maxAllowedPageSize>was deliberately excised; How-to: Cursor-paginated connections covers the alternative.
See also
-
@notGeneratedis the directive surface this recipe expands; it carries the abridged migration matrix in tabular form. -
How-to: Stacking and overriding conditions covers the
@conditioncascade divergence end-to-end. -
How-to: Handle services is the destination for most
@notGeneratedmigrations on root and split-query fields. -
How-to: Result-type variants covers the
@record-parent migration path (accessor inference vs@sourceRow). -
How-to: Global object IDs covers the
@node/@nodeIddeclaration shapes the synthesis shims migrate to.