Search code examples
javarecursiongraphqlgraphql-java

GraphQL Java Annotations recursion issue


I am trying to create a recursive schema using GraphQL Java Annotations, but throws an exception.

import org.junit.Assert;
import org.junit.Test;

import java.util.concurrent.ThreadLocalRandom;

import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.annotations.GraphQLAnnotations;
import graphql.annotations.GraphQLDataFetcher;
import graphql.annotations.GraphQLDescription;
import graphql.annotations.GraphQLField;
import graphql.annotations.GraphQLName;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;

import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;

public class RecursiveSchemaTest {

  @GraphQLDescription("TestObject object")
  @GraphQLName("TestObject")
  public static class TestObject {

    @GraphQLField
    private Integer id;

    @GraphQLField
    @GraphQLDataFetcher(TestObjectDataFetcher.class)
    private TestObject child;

    public TestObject(Integer id) {
      this.id = id;
    }

    public Integer getId() {
      return id;
    }

    public void setId(Integer id) {
      this.id = id;
    }

    public TestObject getChild() {
      return child;
    }

    public void setChild(TestObject child) {
      this.child = child;
    }
  }

  public static class TestObjectDataFetcher implements DataFetcher<TestObject> {

    @Override
    public TestObject get(DataFetchingEnvironment environment) {
      return new TestObject(ThreadLocalRandom.current().nextInt());
    }
  }

  @Test
  public void test() {
    GraphQLObjectType graphQLObjectType = GraphQLAnnotations.object(TestObject.class);
    GraphQLObjectType rootQuery = GraphQLObjectType.newObject().name("data").field(
        newFieldDefinition().name(graphQLObjectType.getName()).type(graphQLObjectType)
            .dataFetcher(new TestObjectDataFetcher()).build()).build();

    GraphQLSchema schema = GraphQLSchema.newSchema().query(rootQuery).build();
    GraphQL graphQL = GraphQL.newGraphQL(schema).build();

    ExecutionResult result = graphQL.execute("{ TestObject { id, child { id , child { id }}");
    Assert.assertFalse(result.getErrors() != null && !result.getErrors().isEmpty());
    Assert.assertNotNull(result.getData());
  }
}

Parsing the class goes through fine, but creating the schema throws the following exception (this line: GraphQLSchema schema = GraphQLSchema.newSchema().query(rootQuery).build();):

graphql.AssertException: All types within a GraphQL schema must have unique names. No two provided types may have the same name.
No provided type may have a name which conflicts with any built in types (including Scalar and Introspection types).
You have redefined the type 'TestObject' from being a 'GraphQLObjectTypeWrapper' to a 'GraphQLObjectTypeWrapper'

    at graphql.schema.SchemaUtil.assertTypeUniqueness(SchemaUtil.java:86)
    at graphql.schema.SchemaUtil.collectTypesForObjects(SchemaUtil.java:122)
    at graphql.schema.SchemaUtil.collectTypes(SchemaUtil.java:56)
    at graphql.schema.SchemaUtil.collectTypesForObjects(SchemaUtil.java:128)
    at graphql.schema.SchemaUtil.collectTypes(SchemaUtil.java:56)
    at graphql.schema.SchemaUtil.collectTypesForObjects(SchemaUtil.java:128)
    at graphql.schema.SchemaUtil.collectTypes(SchemaUtil.java:56)
    at graphql.schema.SchemaUtil.allTypes(SchemaUtil.java:153)
    at graphql.schema.GraphQLSchema.<init>(GraphQLSchema.java:42)
    at graphql.schema.GraphQLSchema$Builder.build(GraphQLSchema.java:130)
    at graphql.schema.GraphQLSchema$Builder.build(GraphQLSchema.java:125)
    at RecursiveSchemaTest.test(RecursiveSchemaTest.java:74)

Any ideas why the schema is not correctly created? I am using the latest versions of both graphql-java (3.0.0) and graphql-java-annotations (0.14.0)


Solution

  • I believe this is a bug with graphql-java-annotation that has already been closed. The previous version of graphql-java allowed for duplicating type names, but as of 3.0.0 it is an error, and the annotations lib hasn't caught up yet.

    The fix should be in the upcoming release...

    Btw, check out my lib, graphql-spqr, it allows for even more automated schema generation and would cover your use-case with ease:

    public static class TestObject {
        private Integer id;
    
        public TestObject(Integer id) {
            this.id = id;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    }
    
    public static class TestObjectService {
    
        @GraphQLQuery(name = "TestObject")
        public TestObject getRoot() { //no GraphQL-specific classes mentioned
            return getRandom();
        }
    
        @GraphQLQuery(name = "child")
        public TestObject getChild(@GraphQLContext TestObject parent) {
            return getRandom();
        }
    
        private TestObject getRandom() {
            return new TestObject(ThreadLocalRandom.current().nextInt());
        }
    }
    
    @Test
    public void test() {
        GraphQLSchema schema = new GraphQLSchemaGenerator()
                .withOperationsFromSingleton(new TestObjectService())
                .generate(); //that's all :)
        GraphQL graphQL = GraphQL.newGraphQL(schema).build();
    
        ExecutionResult result = graphQL.execute("{ TestObject { id, child { id , child { id }}}}"); //your query has a syntax error
        assertFalse(result.getErrors() != null && !result.getErrors().isEmpty());
        assertNotNull(result.getData());
    }
    

    Notice that I removed the child property from TestObject, as it wasn't really being used (as it was replaced by a different fetcher). Still, if you left it alone, there would be no difference - the custom fetcher (nested via @GraphQLContext) would still override it. The idea behind @GraphQLContext is to allow nesting queries without having to cram logic into the model or even ever touching the model objects.

    Fields can also be annotated if you want to rename them or add descriptions, e.g.

    @GraphQLQuery(name = "child", description = "The child object")
    public TestObject getChild() {
        return child;
    }