Skip to content

Allow resolver methods that leave out the source object - #904

Merged
oryan-block merged 5 commits into
masterfrom
feature/304-sourceless-resolvers
Oct 23, 2026
Merged

oryan-block merged 5 commits into
masterfrom
feature/304-sourceless-resolvers

Conversation

@oryan-block

@oryan-block oryan-block commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #304

Checklist

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

Description

A GraphQLResolver<T> method has to take the source as its first parameter, even when it doesn't need it. The case from the issue is a child field that only reads what its parent put in localContext:

class FolderResolver : GraphQLResolver<Folder> {
    fun images(folder: Folder): DataFetcherResult<List<Image>> =
        DataFetcherResult.newResult<List<Image>>().data(folder.images).localContext(folder.supportedFileTypes).build()
}

class ImageResolver : GraphQLResolver<Image> {
    fun imageBlob(env: DataFetchingEnvironment) = load(env.getLocalContext<List<String>>())   // No method found ... today
}

Now a resolver method can leave out the source if it ends in at least one DataFetchingEnvironment, GraphQLContext or context class parameter, after the field's arguments (thumbnail(size: Int, env)). Without one (imageBlob()), it's still not a match, so helpers and Kotlin property getters on resolvers don't suddenly become resolvers.

These methods are only looked at where the build fails today with "No method found", so nothing that builds today binds differently:

  • anything that matches today wins, including a data class getter with the same name (Image.getName() beats ImageResolver.name(env)) and a method that takes the source (title(image) beats title(env)). So it doesn't follow "resolver before data class", which I'd have liked, but that would rebind fields that work today
  • with allowUnimplementedResolvers, missingResolverDataFetcher or missingResolverDataFetcherProvider set, a missing field still gets the missing resolver fetcher. Otherwise a helper like preview(env) would take over a field that builds today. So the feature doesn't work with those options, which is the main limitation here
  • root resolvers already take no source, so nothing changes for them
  • between resolvers of a type and of its supertypes, the order is the same as today (most specific first)

A method is also rejected (so it keeps failing at build time instead of failing every query) when an argument would land on an environment or context parameter (thumbnail(context: GraphQLContext, env) for thumbnail(size: Int), the #890 warning), or when its first parameter could be the source (title(source: Any, env) or title(item: Item, env) on GraphQLResolver<Book>), since that's most likely a mistyped source. Because of the first rule, valid methods never trigger the #890 warning, and it still fires where it did. The "No method found" error now also lists the forms without the source, e.g. ImageResolver.thumbnail(~size, <environment or context>).

The first commit makes "does parameter 0 take the source?" a single decision. The scanner decided it from the search and MethodFieldResolver.getIndexOffset decided it again from the method's declaring class. They only disagreed when the method is declared by the data class itself (or a class both extend), e.g. class Badge : GraphQLResolver<Badge>. Those schemas build but every query of the field fails with Cannot construct instance of Badge, and now they work.

Release note: a resolver method can leave out the source object when it ends in a DataFetchingEnvironment, GraphQLContext or context class parameter, e.g. fun imageBlob(env: DataFetchingEnvironment) in a GraphQLResolver<Image>. It's only used when nothing else matches the field, and not when allowUnimplementedResolvers or a missing resolver data fetcher is set.

SourcelessResolverTest has 14 cases, 6 of them fail without this change and the rest pin what must not change (getter and overload precedence, root ambiguity, the missing resolver options, rejected methods). The Badge case is in MethodFieldResolverTest and fails without it too. mvn clean verify passes on JDK 25 against graphql-java 26.1 and 25.1 (398 tests), and on JDK 11 (397).

Also checked through the installed jar in a separate consumer project, on graphql-java 26.1 and 25.1 at runtime: env with localContext, an argument plus env, GraphQLContext, the context class, both together and a suspend method all resolve (the build fails without this change). A data class getter and an overload with the source, a helper with allowUnimplementedResolvers, the #890 warnings and a root resolver taking only the environment give the same results and log lines as before. A method with an argument on its context parameter still fails, with the extra signature lines. javap only shows internal classes changing (DataClassTypeResolverInfo removed, FieldResolverScanner and Search members).

Schema build JMH on graphql-java 25.1, 3 alternating runs each: no measurable difference. The edealer schema takes 60.3 ms on master (a4736c9), 61.9 ms on #903 and 61.5 ms here (medians, about 2 ms between runs of the same jar), and allocates about 163 MB per build on all three. Expected, since fields that match today don't run any new code.

Base automatically changed from feature/254-nested-wrappers to master October 23, 2026 13:50
@sonarqubecloud

Copy link
Copy Markdown

@oryan-block

Copy link
Copy Markdown
Collaborator Author

Verifier: PASS+NOTES on e0dc4f4 (vs master 84dcb8e), through the installed jar in a consumer project, graphql-java 26.1 and 25.1 at runtime.

  • sourceless (env + localContext, argument + env, GraphQLContext, context class, suspend): fails to build on master, resolves here
  • sourceless-shadowed, sourceless-unimplemented, context-misbound, context-and-environment, bookstore, async, polymorphism, options: identical results and logs
  • sourceless-misbound, bookstore-missing-resolver: still fail, with the extra <environment or context> signature lines

Note: with allowUnimplementedResolvers (or a missing resolver data fetcher) a sourceless method isn't bound. That's the documented limitation.

@oryan-block
oryan-block merged commit d56121f into master Oct 23, 2026
7 checks passed
@oryan-block
oryan-block deleted the feature/304-sourceless-resolvers branch October 23, 2026 16:09
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.

Support for MethodFieldResolver with only DataFetchingEnvironment.

1 participant