ID

R467

Status

Ready

Bucket

tech-debt

Priority

5

Theme

classification-model

Created

2026-07-10

Updated

2026-08-08

Upgrade graphql-java 25.0 → 26.0

Bump graphql-java from 25.0 to 26.0 in the root pom.xml dependency-management block, together with the federation release that unblocks it.

External blocker: cleared

federation-graphql-java-support 7.0.0 is on Maven Central (published 2026-08-03) and declares com.graphql-java:graphql-java:26.0 as a compile dependency, which is the release apollographql/federation-jvm#454 was tracking. Nothing external is outstanding; this item is implementable.

graphql-java-extended-scalars stays at 24.0. 24.0 is still the newest release (the <latest> marker in its Maven metadata points at a stale dated snapshot, not a newer release), and it resolves and behaves correctly against graphql-java 26.

Scope, as verified by a full dry run

The whole change was exercised end to end on a throwaway working tree: the two version bumps plus the fixes below produce a green mvn install -Plocal-db across all 13 modules, tests, docs render and the javadoc reference gate included. The findings below are measured, not predicted.

1. Two version bumps (root pom.xml dependency-management)

graphql-java 25.0 → 26.0 and federation-graphql-java-support 6.2.0 → 7.0.0. These must move together; either alone is incoherent.

2. DirectiveInfo is gone; Directives replaces it

graphql.schema.idl.DirectiveInfo no longer exists in 26.0 (the class file is absent from the jar, and no relocated equivalent carries isGraphqlSpecifiedDirective). Its role moved onto graphql.Directives, which now publishes BUILT_IN_DIRECTIVES, BUILT_IN_DIRECTIVES_MAP and isBuiltInDirective(...) in both the name and GraphQLDirective overloads. The membership set is identical to 25.0’s GRAPHQL_SPECIFICATION_DIRECTIVE_MAP (@include, @skip, @deprecated, @specifiedBy, @oneOf, @defer, @experimental_disableErrorPropagation), so this is a rename, not a behaviour change. graphql-java’s own SchemaPrinter already routes its ExcludeGraphQLSpecifiedDirectivesPredicate through Directives.isBuiltInDirective, which confirms the intended replacement.

One call site, in SchemaSdlEmitter.includeSchemaElement: swap the graphql.schema.idl.DirectiveInfo import for graphql.Directives and the call for Directives.isBuiltInDirective(directive).

OneOfDirectiveSdl’s class javadoc also names `{@code graphql.schema.idl.DirectiveInfo.isGraphqlSpecifiedDirective} in prose when explaining why the federation printer strips the @oneOf definition. That is a {@code} reference, so no gate catches it, but it goes stale with this bump and should be repointed to Directives.isBuiltInDirective in the same commit.

3. additionalType narrowed to GraphQLNamedType

GraphQLSchema.Builder.additionalType narrowed its parameter from GraphQLType to GraphQLNamedType, and additionalTypes correspondingly to Set<? extends GraphQLNamedType>; GraphQLSchema.getAdditionalTypes() now returns Set<GraphQLNamedType>.

One call site, in ConnectionPromoter’s post-walk rebuild. The fix is a strict simplification rather than a workaround: the local `pinned map is declared LinkedHashMap<String, GraphQLType> but every value is already downcast to GraphQLNamedType to read its name, so declaring the map LinkedHashMap<String, GraphQLNamedType> and hoisting that existing cast to the putIfAbsent makes the narrower static type carry what the code already assumed.

26.0 also adds a FastBuilder path and documents that getAdditionalTypes() under it returns all non-root types rather than only explicitly-added ones. The draft left confirming graphitron’s exposure to the reviewer; settled at the gate: getAdditionalTypes() has zero call sites in the reactor, main and test alike, so nothing depends on either meaning and the semantic split stays latent. Graphitron does not use FastBuilder either. Nothing to do here.

4. New: the -Werror warning ratchet trips, and the fix is a policy call

Not predicted at filing time, and the only part of this item that is a genuine decision rather than a mechanical edit. With graphql-java 26.0 on the classpath, compiling the graphitron module emits nine [classfile] warnings and -Werror turns them into a build failure:

graphql-java-26.0.jar(/graphql/com/google/common/collect/ImmutableList.class): warning: [classfile]
  Cannot find annotation method 'serializable()' in type 'GwtCompatible':
  class file for com.google.common.annotations.GwtCompatible not found

The warnings originate entirely inside graphql-java’s own jar, in its relocated graphql.com.google.common.collect.ImmutableList, whose retained annotations still reference the unrelocated com.google.common.annotations and com.google.errorprone.annotations packages that the shaded jar does not bundle. This is not graphitron code and cannot be fixed in graphitron code. It is also not a regression in graphql-java’s shading: 25.0’s relocated ImmutableList carries byte-identical dangling annotation references. What changed is that some resolution path in the graphitron module now reaches that class file where under 25.0 it did not, so javac reads its annotations and reports what it cannot resolve. Only graphitron is affected; every other module compiles clean.

Two remedies, both verified to produce a fully green reactor:

A. Exclude the classfile lint category (-Xlint:all,-classfile in the root pom’s compilerArgs, with the reason recorded in the comment block that already sits there for exactly this purpose). This is the escape hatch the ratchet documents. The argument for it is that classfile is structurally incapable of serving the ratchet’s stated goal: it fires only when javac reads a dependency’s class files, never on Java that graphitron emits, so excluding it does not weaken the cross-module backstop that a generator change emitting warning-producing code fails the build. The cost is losing a genuine signal about malformed dependency jars tree-wide.

The comment block is not append-only here. It currently asserts "No category is excluded: the full mvn install -Plocal-db build is clean across all of them", which remedy A makes false. Rewrite that sentence to name classfile as the one exclusion and why, rather than adding a reason underneath a claim that now contradicts it.

B. Put the annotation artifacts on the compile classpath (com.google.guava:guava and com.google.errorprone:error_prone_annotations, provided scope, on graphitron only). Verified: this clears all nine warnings and graphitron main compiles clean under the untouched -Xlint:all -Werror. It preserves the classfile category. The cost is dragging a multi-megabyte library onto a module’s compile classpath purely so javac can resolve annotation methods it will then discard, plus the standing risk that Guava becomes casually reachable in graphitron source.

Decision: A, recommended by the draft and ratified at the Spec gate, on the reasoning that the excluded category cannot detect the class of defect the ratchet exists to catch, and that a recorded one-line exclusion is a smaller standing liability than a dependency added as a shim for another project’s shading artifact. Implement A; B needs no further consideration.

A third shape was weighed at the gate and rejected: scoping the exclusion to the graphitron module rather than the root pom, which graphitron/pom.xml could carry since it already declares a maven-compiler-plugin <configuration> block. That would erase A’s only stated cost, since the spec establishes that no other module is affected. It was rejected because Maven replaces a child <compilerArgs> list rather than merging it, so the module would have to restate -Xlint and -Werror in a second place, and the ratchet’s arguments would then drift apart silently the next time the root block changes. One source of truth for the ratchet is worth more than the narrower blast radius of an exclusion that, by the argument above, gives up no signal the ratchet can act on.

5. Emitted-code surface (surface A): clean, no generator change

This was the filing’s open question, and it resolves in graphitron’s favour. Once the generator compiles, the full pipeline (generate → release 17 compile of emitted sources → execute) passes: graphitron-sakila-example runs 704 tests across 90 classes with zero failures against a live database.

None of the three emitted APIs the coupling map flagged as highest-risk needed any change: the graphql.execution.instrumentation.* Instrumentation SPI that GraphitronConnectionInstrumentationGenerator emits against, DataFetchingEnvironmentImpl, and ValuesResolver.valueToLiteral in AppliedDirectiveEmitter all still resolve as emitted.

The additionalType narrowing also needs no generator change, which is worth stating explicitly because it is the non-obvious outcome: the generator emits additionalType(XType.type()) and additionalType(graphql.Scalars.GraphQLString), and every such argument is already a GraphQLNamedType, so the emitted call sites bind the narrowed overload as written. Confirmed at the bytecode level: emitted classes compile to additionalType:(Lgraphql/schema/GraphQLNamedType;). No GraphitronSchemaClassGeneratorTest expectation changes, since they assert the emitted argument text, which is unchanged.

One narrow consumer exposure the dry run could not reach, recorded at the gate as a known consequence rather than as work for this item. The @scalarType(scalar: "FQN.FIELD") path emits the consumer’s constant by reference, and ScalarTypeResolver gates it on the reflected value being a GraphQLScalarType, never on the field’s declared type. A consumer whose constant is declared public static final GraphQLType MONEY = ... therefore passes codegen and emits additionalType(ScalarConstants.MONEY), which compiled under 25.0 and does not under 26.0. Every graphitron fixture declares the constant as GraphQLScalarType, which is why nothing in the reactor sees it. The declaration is unusual and the break is graphql-java’s own, so it does not change this item’s scope; tightening the gate to the declared type, so the invariant has an enforcer at the parse boundary instead of in a consumer’s javac, belongs in a separate Backlog item.

6. Not affected

JDK floor is unchanged: 25.0 and 26.0 both publish class-file major version 55, so neither the runtime floor nor the generator’s Java 25 build moves, and generated output stays Java 17. No test expectations change anywhere in the reactor. The graphql-java version the consumer-lag nudge reports is resolved off graphitron’s own plugin realm rather than hardcoded, so the 25.0 strings in DependencyVersionWarningsTest and DependencyVersionDecodeTest are fixture inputs and stay as they are; consumers still on 25.0 start seeing graphql-java-version-lag after this ships, which is the mechanism working as documented in docs/dependencies.adoc.

Implementation note

A NoSuchMethodError cascade on additionalType(GraphQLType) across the execution tier is the signature of stale bytecode, not a real break: a non-clean build after the version bump leaves graphitron-sakila-example class files compiled against 25.0, because regeneration alone does not force recompilation. Use mvn clean install -Plocal-db when verifying this item; a warm incremental build will produce dozens of misleading failures.

Review record

One Spec-review pass, by a session independent of the drafting one. No blocking defect; the item goes to Ready.

Every external claim was re-verified against the published artifacts rather than taken on report: graphql.schema.idl.DirectiveInfo is absent from the 26.0 jar; graphql.Directives publishes BUILT_IN_DIRECTIVES, BUILT_IN_DIRECTIVES_MAP and both isBuiltInDirective overloads, and its membership printed from a live 26.0 classpath is exactly the seven names the body lists, identical to 25.0’s GRAPHQL_SPECIFICATION_DIRECTIVE_MAP printed the same way; additionalType, additionalTypes and getAdditionalTypes carry the narrowed signatures; the relocated ImmutableList is byte-identical between the two jars and its retained annotations do dangle on GwtCompatible and the errorprone package, neither of which the jar bundles; federation 7.0.0 declares graphql-java:26.0 at compile and still ships every one of the eight federation classes graphitron references, ServiceSDLPrinter.generateServiceSDLV2 included; and 24.0 is the newest semantic release of graphql-java-extended-scalars, as is 26.0 of graphql-java. In the tree, the DirectiveInfo import and call in SchemaSdlEmitter.includeSchemaElement, the {@code} mention in OneOfDirectiveSdl’s javadoc, the `pinned map and its existing downcast in ConnectionPromoter, the ratchet comment block, and the emitted-surface symbols in GraphitronConnectionInstrumentationGenerator and AppliedDirectiveEmitter all exist as named.

The gate closed the item’s one open decision in favour of remedy A and recorded the rejected module-scoped variant alongside it, settled the getAdditionalTypes() question the draft left open, and added the @scalarType consumer exposure and the comment-block rewrite requirement. The federation bump is a major version and the body reasons about graphql-java only; that rests on the green dry run rather than on prose, and the guards that make it safe are already in place: SchemaSdlEmitterTest asserts byte-identity against ServiceSDLPrinter.generateServiceSDLV2 and FederationBuildSmokeTest pins the runtime surface at the execution tier.