Apply raw and star-projected resolvers to parameterized data classes - #832
Open
oryan-block wants to merge 1 commit into
Open
oryan-block wants to merge 1 commit into
oryan-block wants to merge 1 commit into
Conversation
A resolver declared for a raw generic data class, such as GraphQLResolver<Page> in Java, was never linked to a field returning Page<Item>. Resolvers were matched by comparing their data class to the field's Java type, and a ParameterizedType never equals a Class, so the resolver's methods were reported as missing. Resolvers for a supertype or interface were skipped for the same reason. Kotlin can't express raw types, and GraphQLResolver<Page<*>> failed earlier with "Unable to determine data class" because the type argument isn't a Class. Match resolvers, including supertype resolvers, against the raw class of the data class type, and keep the parameterized type for the data class search so that fields typed by a type variable still resolve. A type whose type arguments are all unbounded wildcards is now treated as its raw type, both for the resolver's data class and for the source parameter of its methods, so Page<*> and Page<?> behave like Page. Resolvers for a specific parameterization are still not supported: a GraphQLResolver<Page<Item>> now fails with an error pointing to the raw type or unbounded wildcards instead of reporting a library bug, and a method taking Page<Item> as its source is not matched, since it would also be called for other parameterizations. Fixes #308 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #308
Checklist
Description
A resolver for a generic data class is never linked to fields that return a parameterization of it. In #308,
TestResolver implements GraphQLResolver<TestDataClass>is ignored for a query returningTestDataClass<MyType>. So withGraphQLResolver<Page>and a field returningPage<Item>, the build fails withFieldResolverError: No method or field found as defined in schema ...for the fields the resolver provides. If the data class has its own getter, the resolver just sits there with the "Resolver was provided but no methods on it were used" warning. Kotlin can't express raw types at all, andGraphQLResolver<Page<*>>fails even earlier withResolverError: Unable to determine data class for resolver ... This is most likely a bug with graphql-java-tools.SchemaClassScanner.getResolverInfoFromDataClassmatched resolvers withit.dataClassType == dataClass. For a field returningPage<Item>,dataClassis aParameterizedType, which never equals the resolver'sClass.isResolverForSupertypealso requireddataClass is Class<*>, so resolvers for a supertype or interface were skipped too. On top of that,NormalResolverInfo.findDataClassonly accepted aClassas theGraphQLResolvertype argument, and Kotlin'sPage<*>compiles toPage<?>.Resolvers, including supertype and interface ones, are now matched against the raw class of the data class type.
MultiResolverInfokeeps the parameterized type for the data class search, so fields typed by a type variable (content: List<T>) still resolve. With the raw class they fail with "Could not resolve type variable ... declaring type is not parameterized". A neweraseUnboundedWildcards()helper turns a type whose type arguments are all unbounded wildcards into its raw type.findDataClassuses it, soGraphQLResolver<Page<*>>andGraphQLResolver<Page<?>>behave likeGraphQLResolver<Page>.verifyMethodArgumentsuses it for the source parameter, sogetSize(page: Page<*>)is accepted. Kotlin emits an unboundedPage<?>forPage<*>even whenThas an upper bound, so star projection always works. The raw resolver test is inResolverMethodsTest.javasince Kotlin can't express raw types.I didn't add resolvers for one specific parameterization (
GraphQLResolver<Page<Item>>, or bounded wildcards likePage<out Foo>). Picking a resolver per type argument needs a design decision. Those now fail with "Resolver 'X' may not have a parameterized type (Page) as its type, use the raw type or unbounded wildcards (<?> in Java, <*> in Kotlin) instead." instead of pointing at a library bug. Bounded wildcards aren't treated as raw, since that would apply aPage<? extends Foo>resolver toPage<Bar>. For the same reason, a source parameter typedPage<Item>on aPage<*>resolver still isn't matched, and the build fails withFieldResolverErrorlike on master. Accepting it would call the method forPage<User>too and throw aClassCastExceptionat query time.GenericTypeisn't touched, so this doesn't overlap with #825.Behaviour change: a resolver for a raw generic class, for
Foo<*>/Foo<?>, or for any of its supertypes or interfaces now applies to fields returning a parameterization of that class. Before, it was ignored. So if both the resolver and the data class define the same field, the resolver method now wins, like it does for non-generic data classes. Supertype resolvers already applied to non-generic subclasses since #820, and now they apply to parameterized ones too. If such a resolver method replaces a data class getter, the class found for a field's type can change, and a schema that used to build can fail with "Two different classes used for type X". For example,BaseResolver : GraphQLResolver<Base>withchildren(b: Base): List<Child>, aHolder<T> : Basewithchildren: List<T>, and a query returning bothHolder<SpecialChild>andSpecialChild. That builds on 14.1.0 and fails with this change.🤖 Generated with Claude Code