graphql-java-kickstart / graphql-java-tools

A schema-first tool for graphql-java inspired by graphql-tools for JS
https://www.graphql-java-kickstart.com/tools/
MIT License
810 stars 174 forks source link

Building schema parser fails when java.util.List is used with a type parameter from a generic class #460

Open devopsix opened 3 years ago

devopsix commented 3 years ago

Affected version: 6.2.0

Description

I am having difficulties backing an input object with a Java class that inherits a java.util.List<T> property from its generic super class.

Given this schema:

type Query {
}

type Mutation {
  mutate(input: MutationInput!): String!
}

input MutationInput {
  items: [Item!]!
}

input Item {
  prop: String!
}

And given this generic base class:

package com.example.generics;

import java.util.List;

public abstract class GenericMutationInput<T> {
    public List<T> items;
}

And given this backing class for the MutationInput input object:

package com.example.generics;

public class MutationInput extends GenericMutationInput<MutationInput.Item> {
    public static class Item {
        public String prop;
    }
}

Then building the SchemaParser object fails with this exception:

Caused by: java.lang.IllegalStateException: TypeUtils.getRawType(type, declaringType) must not be null
        at graphql.kickstart.tools.GenericType$RelativeTo.replaceTypeVariable(GenericType.kt:156) ~[graphql-java-tools-6.2.0.jar:na]

It seems the parameters passed to TypeUtils.getRawType(Type, Type) do not carry sufficient information for determining the raw type. The required information would to be contained in this.mostSpecificType.

image

A few things I have already tried:

Steps to reproduce the bug

I have created a minimal Spring Boot application for reproducing this issue.

Given you have OpenJDK 15 installed.

  1. Extract attached ZIP file.
  2. Run ./mvnw clean spring-boot:run "-Dspring-boot.run.profiles=generics".

Expected behavior: Starting the Spring Boot application succeeds.

Actual behavior: Starting the Spring Boot application fails with this root cause:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [graphql.kickstart.tools.SchemaParser]: Factory method 'schemaParser' threw exception; nested exception is java.lang.IllegalStateException: TypeU
tils.getRawType(type, declaringType) must not be null
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.3.1.jar:5.3.1]
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:651) ~[spring-beans-5.3.1.jar:5.3.1]
        ... 138 common frames omitted
Caused by: java.lang.IllegalStateException: TypeUtils.getRawType(type, declaringType) must not be null
        at graphql.kickstart.tools.GenericType$RelativeTo.replaceTypeVariable(GenericType.kt:156) ~[graphql-java-tools-6.2.0.jar:na]
        at graphql.kickstart.tools.GenericType$RelativeTo.replaceTypeVariable(GenericType.kt:143) ~[graphql-java-tools-6.2.0.jar:na]
        at graphql.kickstart.tools.GenericType$RelativeTo.unwrapGenericType(GenericType.kt:86) ~[graphql-java-tools-6.2.0.jar:na]
        at graphql.kickstart.tools.TypeClassMatcher.match(TypeClassMatcher.kt:28) ~[graphql-java-tools-6.2.0.jar:na]

Starting the application with an alternative profile that replaces the backing class with a “non-generic” one succeeds: ./mvnw clean spring-boot:run "-Dspring-boot.run.profiles=nogenerics".

marcust commented 3 years ago

The same issue happens when the type that it tries to figure out is actually generic over multiple levels:

type Query {
  item: Item!
}

type Item {
   id: ID!
}

And backing Java classes like:

class AbstractItem<T> {
   T id;
}

class BaseItem<T> extends AbstractItem<T> {}
class ConcreteItem extends BaseItem<Long> {}

leads to the same exception

Caused by: java.lang.IllegalStateException: TypeUtils.getRawType(type, declaringType) must not be null
    at graphql.kickstart.tools.GenericType$RelativeTo.replaceTypeVariable(GenericType.kt:156)
    at graphql.kickstart.tools.GenericType$RelativeTo.unwrapGenericType(GenericType.kt:86)
    at graphql.kickstart.tools.TypeClassMatcher.match(TypeClassMatcher.kt:28)
    at graphql.kickstart.tools.TypeClassMatcher.match(TypeClassMatcher.kt:23)
    at graphql.kickstart.tools.SchemaClassScanner.scanResolverInfoForPotentialMatches(SchemaClassScanner.kt:274)
    at graphql.kickstart.tools.SchemaClassScanner.scanQueueItemForPotentialMatches(SchemaClassScanner.kt:264)
    at graphql.kickstart.tools.SchemaClassScanner.scanQueue(SchemaClassScanner.kt:113)
    at graphql.kickstart.tools.SchemaClassScanner.scanForClasses(SchemaClassScanner.kt:74)
    at graphql.kickstart.tools.SchemaParserBuilder.scan(SchemaParserBuilder.kt:154)
    at graphql.kickstart.tools.SchemaParserBuilder.build(SchemaParserBuilder.kt:195)
    at graphql.kickstart.tools.boot.GraphQLJavaToolsAutoConfiguration.schemaParser(GraphQLJavaToolsAutoConfiguration.java:152)
laszlopalfi commented 3 years ago

Hello guys, I'm experiencing the same issue, any idea if this is going to be fixed?

cornwe19 commented 3 years ago

It seems like this issue also happens when a generic has a non-generic, non-concrete (interface or abstract class) type as well. For example,

interface A { val name: String }

class B: A {
    override val name: String = "B"
}

class C: A {
    override val name: String = "C"
}

class Resolver: GraphQLQueryResolver {
    fun listAInstances(): Page<A> = ...
}

Schema

type Query {
    listAInstances: APage
}

type APage {
    contents: [A]
}

type A {
    name: String
}

Is there any way to work around this without completely duplicating the graphql APage and A types in this situation? Or any update on when this might be resolved?