graphql-java-tools icon indicating copy to clipboard operation
graphql-java-tools copied to clipboard

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

Open devopsix opened this issue 3 years ago • 3 comments

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:

  • It does not matter if Item is a nested class or not. The issue occurs in either case.
  • The issue does not occur if String is used for type parameter T instead of MutationInput.Item.
  • The issue does not occur if MutationInput has an own property java.util.List<MutationInput.Item> and has no generic super class:
    package com.example.nogenerics;
    
    import java.util.List;
    
    public class MutationInput {
        public static class Item {
            public String prop;
        }
        public List<Item> items;
    }
    
  • The issues does not occur if MutationInput overrides getter and setter for items (getters and setters not shown above for brevity).

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".

devopsix avatar Nov 28 '20 23:11 devopsix

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)

marcust avatar Jan 06 '21 16:01 marcust

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

laszlopalfi avatar Jul 13 '21 11:07 laszlopalfi

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?

cornwe19 avatar Aug 31 '21 18:08 cornwe19