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, or, in one case, resolves differently with no error at all), the locations that still warn today and 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.

Names from inside the generator. This page names types from graphitron’s own classification model. They are accurate today, and worth knowing when a rejection message quotes one back at you, but they are not part of the authoring contract: the classification walk that produces them is being drained, and these names retire with it. What stays stable is the schema you write, the directive reference that describes it, and the diagnostics glossary.

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 and supports the following directives. Each is documented in the architecture chapter:

  • @asConnection

  • @asFacet

  • @condition

  • @defaultOrder

  • @discriminate

  • @discriminator

  • @enum

  • @error

  • @externalField

  • @field

  • @index

  • @lookupKey

  • @mutation

  • @node

  • @nodeId

  • @order

  • @orderBy

  • @pivot

  • @record

  • @reference

  • @referenceFor

  • @routine

  • @scalarType

  • @service

  • @splitQuery

  • @table

  • @tenantFanOut

Removed / rejected directives

These directives are still declared so existing schemas parse, but the rewrite no longer supports them: applying one is a build error. Remove them from your schema (see the migration guide for replacements):

  • @multitableReference

  • @notGenerated

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

  • @tableMethod

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)

@mutation

  • + multiRow: Boolean = false (rewrite only)

  • + table: String (rewrite 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

  • DmlTableField: The direct-return DML mutation: @mutation(typeName:) on a field returning the

  • MutationBulkDmlRecordField: A record-returning DML mutation with bulk DML input and a list-shaped

  • MutationDmlRecordField: A record-returning DML mutation: the schema field carries

  • MutationRoutineWriteField: A mutation field whose table chain starts with a database routine: the routine call

  • MutationRoutineWriteRecordField: A mutation field whose write is a hop-less @routine call and whose return is a

  • MutationServicePolymorphicField: A mutation field backed by a developer-provided service method that returns a multitable

  • 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

  • MutationServiceTableInterfaceField: The mutation analogue of QueryField.QueryServiceTableInterfaceField: a root

  • QueryInterfaceField: A root query field returning a multi-table GraphitronType.InterfaceType

  • QueryNodeField

  • QueryNodesField

  • QueryServicePolymorphicField: A root query field backed by a developer-provided service method that returns a multitable

  • 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

  • QueryServiceTableInterfaceField: A root @service field returning a single-table discriminated interface

  • QueryTableField: A root table read whose FROM starts at the return type’s own table or a routine chain

  • QueryTableInterfaceField

  • QueryUnionField: A root query field returning a multi-table GraphitronType.UnionType

ChildField

  • BatchedInterfaceField: A list- or connection-cardinality child field returning a multi-table

  • BatchedPivotField: The batched (@splitQuery) @pivot leaf: the same projection as

  • BatchedTableField: A DataLoader-batched keyed re-query anchor: the field launches its own SELECT keyed on a

  • BatchedTableInterfaceField: The DataLoader-batched half of the TableInterfaceField delivery split: one keyed

  • BatchedUnionField: A list- or connection-cardinality child field returning a multi-table

  • ColumnBackedField: A column-backed output carrier on a table-backed parent: the field’s value is produced

  • ColumnBackedReferenceField: A column-backed output carrier on a table-backed parent reached through a

  • ComputedField: A child field using @externalField: the developer provides a static method

  • ErrorsField: The errors field on a payload type. Lift target for the payload-side of a

  • InterfaceField: A single-cardinality child field returning a multi-table

  • NestingField: A nesting child field whose value is a fragment of the parent’s table-bound projection

  • ParticipantColumnReferenceField: A scalar field on a GraphitronType.TableInterfaceType participant that

  • PivotField: The inline @pivot leaf: a discriminator-keyed aggregate projection folded into the

  • (not yet supported) PivotSlotField: One projection slot of a @pivot field’s return type, carrying exactly one fact: its

  • RecordCompositeField: The single data field on an @service record-composite carrier payload. The

  • RecordReadField: A value read off a record-backed parent’s in-memory source object: the scalar / enum /

  • 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

  • SingleRecordIdField: The single data field on an @service source-record carrier: an ID-typed scalar

  • SingleRecordIdFieldFromReturning: The single data field on a payload-returning DELETE carrier: an ID-typed scalar encoding

  • TableField

  • TableInterfaceField: A child field returning a GraphitronType.TableInterfaceType (@table

  • UnionField: A single-cardinality child field returning a multi-table

InputField

  • ColumnBackedField: An input field successfully resolved to one or more SQL columns on the consuming

  • ColumnBackedReferenceField: An input field that uses @reference (or an

  • ConditionOwnedField: Input field whose explicit @condition(override: true) method owns the WHERE

  • NestingField: An input field whose GraphQL type is itself an input

  • UnboundField: Input field that does not bind to a SQL column: the classifier looked

GraphitronType

  • Backed: A payload carrier whose backing class was reflected from its

  • ConnectionType: A Relay connection object type — the outer wrapper whose fields are

  • EdgeType: A Relay edge object type — the inner wrapper whose fields are cursor: String! and

  • EnumType: A GraphQL enum type, recorded so schema.types() is complete for emission

  • ErrorType: An object type annotated with @error. Maps Java exceptions to GraphQL error responses

  • FacetValueType: A synthesised, cross-schema-reusable facet value type, e.g

  • FacetsType: The synthesised per-connection facets container, e.g

  • InterfaceType: An interface with no directives. Participating types may be table-bound (ParticipantRef.TableBound)

  • JavaRecordInputType: A non-table input type backed by a Java record class

  • JavaRecordType: A result type whose producer’s reflected return is a Java record class

  • JooqRecordInputType: A non-table input type backed by a jOOQ Record<?> (not table-bound)

  • JooqRecordType: A result type whose producer’s reflected return is a jOOQ Record<?> (not table-bound)

  • JooqTableRecordInputType: A non-table input type backed by a jOOQ TableRecord<?>

  • JooqTableRecordType: A result type whose runtime carrier is a jOOQ record of a resolved table

  • NestingType: A nesting projection: a directiveless SDL object type embedded under a @table-bound

  • NodeType: A GraphQL object type carrying @table that has published the Relay Node

  • PageInfoType: The Relay PageInfo object type. Exactly one instance per schema; the classifier

  • PojoInputType: A non-table input type backed by a plain Java class (POJO), or one for which no producer

  • RootType: A root operation type (Query or Mutation). Unmapped — no source context, no SQL until

  • ScalarType: A GraphQL scalar type whose Java type and GraphQLScalarType constant (or inline

  • TableInterfaceType: An interface annotated with @table and @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

One replacement pointer for the "Legacy-only directives" list above: legacy @tableMethod bound a consumer-authored static method returning a jOOQ table. The rewrite does not declare it. Where the method wrapped a database function, use @routine, which binds the generated Routines-class call directly; where it applied a filter, express that with @condition.

No migration needed: bare id on a node type

No migration needed, and this is the one legacy shape that became more supported rather than less. A bare id: ID! on a node type is the Relay Node interface’s own field, and graphitron reads it as that node’s global ID by construction, with no directive and no warning:

# Supported, no directive required
type Customer implements Node @table(name: "customer") @node {
    id: ID!
    customerId: Int! @field(name: "CUSTOMER_ID")
}

Writing @nodeId on that field is still legal and means exactly the same thing, so existing schemas that spell it out need no edit either. What you cannot write there is @nodeId(typeName:): the enclosing type already answers which node the field identifies, so naming one is either a restatement or a contradiction, and the build rejects the argument rather than checking it.

The implicit reading stops at Node.id. A different bare ID field on the same type (externalId: ID) used to be synthesised into a second global-ID carrier with a WARN; it is an ordinary column-mapped scalar now, so a field with no column of that name fails with column 'externalId' could not be resolved instead of silently encoding. Add @nodeId if you meant a node id, or @field(name: "…") to point at the column.

Hard removals

Two directives the rewrite refuses to accept, one argument it no longer declares, and one inference it no longer performs. The two directives stay declared so the SDL still parses, but every application is rejected at classify time with a "no longer supported" message. The argument is a harder break: the schema does not load at all. The retired inference is quieter than either, and is called out as such below.

@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

field: T @notGenerated on Query / Mutation

@service

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.

field: T @notGenerated on a child of a @table parent

@externalField (column-shaped) or @reference (FK-driven)

@externalField returns a Field<T> from a static method (the framework projects); @reference joins through the catalog. How-to: Join with references covers the FK-path mechanics.

field: T @notGenerated on a class-backed parent

A typed Java accessor

The class-backed parent (its backing class reflected from its producer) must expose the data through a typed Java-record accessor (auto-derived BatchKey). How-to: Result-type variants covers the four backing-class shapes.

field: T @splitQuery @notGenerated (custom batch fetcher)

@service (with @splitQuery for non-root batching)

Service methods can be batched with @splitQuery at non-root sites; the developer-supplied method runs once per parent batch. The contract is Set<Key> → Map<Key, Value>; see the @splitQuery section of How-to: Handle services.

interface T @notGenerated / union U @notGenerated

(no replacement needed)

The legacy "skip interface/union wiring" axis was always tied to RuntimeWiring-side type resolvers; the rewrite always generates these from @discriminate / @discriminator. Remove the directive.

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.

name: on ExternalCodeReference

Unlike the two directives above, this one is not even parsed: the argument is undeclared, so the schema fails to load before classification runs.

# Legacy form, no longer supported
@service(service: {name: "SampleQueryService", method: "filmsByService"})

Replaced with className::

# Canonical form
@service(service: {className: "no.sikt.graphitron.rewrite.test.services.SampleQueryService", method: "filmsByService"})

className: takes the FQCN. The legacy name: was a short name resolved through the plugin’s externalReferences: config; the rewrite briefly carried the argument as a deprecated alias resolved through its own <namedReferences> parameter, and both are now gone. The failure names the offending field:

uses an illegal value for the argument 'service' on directive 'service'.
Fields ['name'] not present in type 'ExternalCodeReference'.

The migration is mechanical: copy the FQCN from the legacy externalReferences: entry into className: at every site, then delete the plugin config block. This applies to all four directives that take an ExternalCodeReference (@service, @externalField, @condition, @enum), including a @condition nested in a @reference path step.

[ID!] @field(name: "X_ID") on filter input fields

Legacy input types let an ID! (or [ID!]) field carry a column name through @field(name:), and the FK qualifier was inferred from the catalog so the value was decoded as a global ID. That inference is removed: @field(name:) now means what it says everywhere, binding the named column and nothing more, so a field written this way silently becomes an ordinary column filter over the raw value.

This is the one change in this recipe with no build error attached, because both readings resolve. Audit filter inputs whose ID fields carry @field(name:) against an FK column.

Before:

# Decoded a global ID via the FK qualifier. Now binds CUSTOMER_ID as a plain column.
input CustomerFilter {
    customerIds: [ID!] @field(name: "CUSTOMER_ID")
}

After:

# Canonical form
input CustomerFilter {
    customerIds: [ID!] @nodeId(typeName: "Customer")
}

When the FK is ambiguous (more than one path from the consuming field’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.

An input field named for its target’s own id needs no directive at all: id: ID! on an input consumed against a node-backed table reads as that node’s global ID, the same way Node.id does on the type. Where the table backs more than one node type the build fails and asks for @nodeId(typeName:), rather than picking one.

WARN today, error later

Paths that still work in the rewrite for backwards compatibility but emit a per-occurrence WARN at build time. Plan to migrate them before the retirement gate fires (no committed date).

@table on input types

Legacy schemas bound every mutation and filter input to a table with @table on the input type. The rewrite deprecated that location: an input’s fields resolve against each consuming field’s table, so the input-side declaration is redundant, and an input reused across consumers resolves per-consumer. The directive is accepted and then ignored, with a per-usage build warning, and will be rejected in a future release. @table on OBJECT and INTERFACE is unchanged.

Because it is ignored rather than honoured, an input whose @table named a different table from the one its consuming field resolves does not migrate silently-but-correctly: the declared table is discarded and the consumer’s is used. The warning is the only signal, so read it instead of assuming the two agreed.

The migration is per-consumer:

  • Filters and lookup inputs: delete the @table; nothing replaces it. The fields resolve against the consuming query field’s return-type table.

  • INSERT / UPDATE mutation inputs: delete the @table; the write target is derived from the mutation field’s return type (a @table return, or a payload whose data field is a @table element). If the return names no table (an encoded-ID / scalar return), add @mutation(table: "…") on the field.

  • DELETE mutation inputs: delete the @table and add @mutation(table: "…") on the field (a DELETE cannot derive its table from the return type).

The @table reference page carries the deprecation statement; @mutation documents the write-target derivation.

@table alone 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. This behavior was retired after a mass typeId collision: ~200 backing types each carrying __NODE_TYPE_ID = "195" were promoted in lockstep and then symmetrically demoted to UnclassifiedType.

The current rule: metadata alone promotes nothing, and implements Node is the opt-in. A @table type that does not declare the interface is a regular TableType no matter what its jOOQ class publishes, which is what keeps a nesting projection over a node-bearing table from becoming a second node. A @table type that does declare implements Node over a table publishing that metadata is a node, and takes typeId and keyColumns from the catalog.

So if a schema relied on the auto-promotion (the type was reachable via Query.node or carried @nodeId fields), add implements Node to the declaration. You need @node as well only when the backing jOOQ class publishes no metadata, or when you want to override a published value; the @node reference covers both. Fields inside the type that depend on node behavior follow the rules in the previous two sections.

One case to expect on this path: if the table also has a column literally named id, the type’s id field is ambiguous between the encoded global ID and that column, and the build fails until you write @nodeId or @field(name: "id") on it. The error names both remedies.

@record is now a no-op

Legacy schemas used @record(record: {className: "…​"}) to bind a class-backed SDL type to its Java class. The rewrite now derives that binding by reflection from the type’s producer instead: for an output type, the producing field’s return type (a @service method return or a parent-accessor chain); for an input type, the method parameter the input flows into. The directive still parses (so existing schemas keep loading), but it is ignored.

A reachable type still carrying @record triggers a build WARN telling you to remove it. The WARN distinguishes three cases:

Case Meaning

redundant

Reflection derives the same class the directive named. Remove the directive; nothing else changes.

shadowed by @table

The type also carries @table, which wins. Remove the @record.

disagrees

Reflection’s class differs from the one the directive named. Reflection wins; the directive is ignored. Remove the directive and verify the producer’s type is the one you want.

Migration is mechanical: delete the @record(record: {…​}) annotation. Make sure the type’s producer (the @service return, or the @externalField lift’s Field<…​> element type) is the Java class you intend to back the type. How-to: Result-type variants covers the reflected backing-class shapes; the @notGenerated-on-a-class-backed-parent row above covers the child-fetch directives that remain unchanged.

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".

@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 filmsOuterOverrideFilterInput 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 on OBJECT / INTERFACE, @field, @reference: unchanged; the catalog-driven mechanics are identical. (@table on input types is deprecated and ignored; see above.)

  • @service, @externalField, @enum: unchanged surface; the only change is the name:className: migration on ExternalCodeReference (above). (@record is the exception: it is now a no-op; see "`@record` is now a no-op" above.)

  • @discriminate / @discriminator: unchanged; the rewrite generates the type resolvers and runtime dispatchers automatically (no RuntimeWiring-side wiring needed).

  • @asConnection: unchanged synth-vs-declared shape; only connectionName: 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 migration work is the @field(name:) inference removal above, plus the two shapes it lists. 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, and description: keeps its Graphitron 9 contract of replacing the exception’s message on message:. Two classify-time rejections are new: duplicate handler tuples, and description: on a {handler: VALIDATION} entry (the validator emits one error per constraint violation, each with its own interpolated message, so put the client-facing string on the constraint annotation’s message attribute). How-to: The errors channel covers the dispatch loop and the resolution order for message:.

  • @lookupKey, @splitQuery: unchanged. Mid-migration schemas that mixed @splitQuery with @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:

  1. Migrate name: to className: on every ExternalCodeReference instance and drop the corresponding entries from externalReferences: plugin config. Do this first: the argument is undeclared, so the schema does not load at all until it lands, and none of the later steps can be observed before it does.

  2. Run a mvn install to surface every @notGenerated and @multitableReference rejection, and every @table-on-input warning. Replace each @notGenerated with the directive named in the matrix above. The build won’t compile under the rewrite until the two rejections are cleared; the @table-on-input warnings do not block it, but clear them too (per the per-consumer steps above) before the location is rejected in a future release.

  3. Audit input types and arguments for ID-typed slots carrying @field(name:) on a foreign-key column; replace with @nodeId(typeName: "T"). Nothing in the build points at these: the inference that used to decode them was removed, so they now bind the raw column and resolve without an error. This is the one step you cannot drive from the build output.

  4. Drop connectionName: arguments from @asConnection and let each field own its connection type.

  5. Migrate @index to @order(index:) on enum values.

  6. 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.

Step 1 is non-optional and comes first (the schema does not load at all). Step 2’s two rejections are non-optional too (the build fails); its @table-on-input half warns only, and will become non-optional when that location is rejected. Step 3 is the one to do deliberately rather than reactively, since a schema that skips it keeps building and changes meaning. Steps 4-5 produce deprecation notices but no failures. Step 6 only matters for schemas that relied on the divergence; most schemas don’t.

Pitfalls

  • @notGenerated is 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.

  • The @field(name:) inference removal leaves no trace to grep for. An ID slot that used to decode a global ID now binds its column, and both readings resolve, so neither the build log nor the language server flags it. Auditing the SDL is the only way to find these; see step 3 of the sequence above.

  • @table alone no longer auto-promotes to NodeType. If a previously-promoted type breaks under the rewrite (typically: Query.node returns null for what used to resolve, or @nodeId synthesis stops firing), add implements Node to opt in. Add @node on top of that only if the backing jOOQ class publishes no _NODE* metadata.

  • Deprecated arguments still work; removed ones do not. A deprecation notice from graphql-java is informational, not blocking, so those migrations are your call until the deprecation flips to removal in a future release. ExternalCodeReference.name: has already made that flip: it is undeclared, and a schema carrying it does not load.

  • The @condition cascade 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. Audit override: true sites with nested-input @condition annotations 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>, and <jooqPackage> 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. The legacy externalReferences: mappings have no rewrite equivalent at all, since the argument they served is gone.

See also