Skip to content

Resolve type variables in resolver return types - #883

Merged
oryan-block merged 1 commit into
masterfrom
refactor/resolver-types
Oct 19, 2026
Merged

oryan-block merged 1 commit into
masterfrom
refactor/resolver-types

Conversation

@oryan-block

Copy link
Copy Markdown
Collaborator

Checklist

  • Pull requests follows the contribution guide
  • New or modified functionality is covered by tests

Description

Same problem as #880, but for return types. MethodFieldResolver used the return type of a method inherited from a generic class without resolving the class's type variables, so it saw T where the scanner saw Future<Any> or EventStream. Two cases broke because of it:

  1. A non-CompletionStage future behind a type variable got handed to graphql-java as the value, with no error. I found this while reviewing Resolve type variables in resolver parameter types #882.
open class Names<T>(private val name: T) {
    fun name(): T = name
}

class Query : Names<Future<Any>>(FutureTask<Any> { "generic" }.also { it.run() }), GraphQLQueryResolver

{ name } with name: String returned "java.util.concurrent.FutureTask@4554de02[Completed normally]". As a union it failed in the type resolver instead. The check that rejects these futures only treats the value as a wrapper when the method's return type is Future, and it read T. Now it fails with the same error as a method declared to return Future<Any>:

Resolver method 'scenarios.Names.name' resolved to a java.util.concurrent.FutureTask, which graphql-java can't wait on. Return a CompletionStage, such as a CompletableFuture, from the method or its generic wrapper transformer instead.

  1. A subscription method whose return type is a type variable bound to a Publisher subtype failed the build:
class EventStream(name: String) : Publisher<Event> by ...

open class Events<P>(private val events: P) {
    fun onEvent(): P = events
}

class EventSubscription : Events<EventStream>(EventStream("inherited")), GraphQLSubscriptionResolver

SchemaClassScannerError: Two different classes used for type Event:
- scenarios.EventStream: return type of method public final java.lang.Object scenarios.Events.onEvent()

(or "No method or field found" depending on the schema). asPublisher, which turns EventStream into Publisher<Event>, ran before the type variables were resolved, so it got P and did nothing. Now it builds and streams the event.

The fix: getReturnType() is now a lazy returnType that resolves the type variables with GenericTypeResolver#resolveTypeVariables, same as getMethodParameterType since #882. getResolvedReturnType (incl. asPublisher) and the Future check both start from it. It's lazy because FieldResolverScanner#findFieldResolver builds a resolver per search and may only use the first one.

This is the first of a few PRs that put the scanner and the data fetchers on one code path for a resolver's Java types. Nothing that builds and works today should change. No public API change, only private members of the internal MethodFieldResolver.

Release note: Resolver methods inherited from a generic class now resolve the class's type variables in their return type too. A non-CompletionStage Future behind a type variable now gets the same error as a declared one instead of returning its toString(), and subscription methods returning a Publisher subtype through a type variable now build.

Testing

Folded both cases into existing tests: inheritedSearch in ReactiveTest and onInheritedPublisher in SchemaParserTest's publisher subtypes test. Both failed before the change. mvn clean verify passes on JDK 25, on JDK 25 against graphql-java 25.1, and on JDK 11.

Also checked through the public API with the classes above, on graphql-java 26.1 and 25.1, before and after. Before: name was the FutureTask string and the subscription schema didn't build. After: the error above, and {"onEvent":{"name":"inherited"}}. A CompletableFuture behind a type variable resolves the same in both.

Schema build benchmark (JMH, graphql-java 25.1) didn't get slower: large schema 37.5 ± 1.0 ms vs 39.3 ± 1.1 on master, same allocation per op.

@sonarqubecloud

Copy link
Copy Markdown

@oryan-block
oryan-block merged commit 94a98a0 into master Oct 19, 2026
7 checks passed
@oryan-block
oryan-block deleted the refactor/resolver-types branch October 19, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant