Search code examples
graphql-java

How to define a type with list of java.util.objects in graphql-java


I have a schema where the json can have a list of Int and Strings as values. I am wondering how to define a type of resolver for my scenario.

type Total {
  key: String
  values: [Object]
}

type Query {
  filter (key : String!) : Total
}

Solution

  • There is no concept of general object (as Object in Java) in GraphQL. In fact, the defined type are the following (as described here):

    1. Int: A signed 32‐bit integer;
    2. Float: A signed double-precision floating-point value;
    3. String: A UTF‐8 character sequence;
    4. Boolean: true or false;
    5. ID: The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be human‐readable.

    You could define a new scalar but it means that you will have to define yourself how it would be serialized/deserialized and validated. It add way more complexity for not much value. Moreover, I feel that the strong typing of Graphql is completely lost with this specific type.

    I think your best bet is to use the Interfaces. With an interface and using wrapper types, you are then able to model this general object that you are talking about, and would help you write a schema as follow:

    interface MyInterface {
        // must have a field at least (cannot be empty)
        id: ID!
    }
    
    type MyInt implements MyInteface {
        // Inherited by MyInterface
        id: ID!
    
        myIntValue: Int
    }
    
    type MyString implements MyInteface {
        // Inherited by MyInterface
        id: ID!
    
        myStringValue: String
    }
    
    type Total {
        key : String
        values: [MyInterface]
    }
    
    type Query {
        filter(key: String!) : Total
    }
    

    Careful: please note that the interface isn't allowed to be empty (no fields). For that purpose, I added an ID field that is by implementation randomly generated.

    With such a schema, we are able to perform a query like the following:

    filter("someFilter") {
        key
        values {
            ... on MyString {
                myStringValue
            }
            ... on MyInt {
                myIntValue
            }
        }
    }
    

    For that to work you would need to define a TypeResolver, as follow:

    private class MyInterfaceTypeResolver implements TypeResolver {
        @Override
        public GraphQLObjectType getType(TypeResolutionEnvironment env) {
            Object obj = env.getObject();
    
            if (obj instanceOf MyInt) {
                env.getSchema().getObjectType("MyInt");
            } else if (obj instanceOf MyString) {
                env.getSchema().getObjectType("MyString");
            } else {
                return null;
            }
        }
    }
    

    with the following Java types definition:

    abstract class MyInterface {
        private String id;
    
        public MyInterface() {
            // Generated just to avoid empty graphql interface
            id = UUID.randomUUID().toString();
        }
    
        public String getId() {
            return id;
        }
    }
    
    class MyInt extends MyInterface {
        private Integer myIntValue;
    
        public MyInt(Integer myInt) {
            super();
            this.myIntValue= myInt;
        }
    
        public Integer getMyIntValue() {
            return myInt;
        }
    }
    
    class MyString extends MyInterface {
        private String myStringValue;
    
        public MyString (String myString) {
            super();
            this.myStringValue= myString;
        }
    
        public String getMyStringValue() {
            return myStringValue;
        }
    }
    
    

    Assuming that you want to represent the following Json {"key": "myKey", "values":[12,"String1", 1, "String2"]}, given our previous model and query, it would be represented like this:

    {
       "data": {
           "filter": {
               "key": "myKey",
               "values" : [
                   {
                       "myIntValue": 12
                   },
                   {
                       "myStringValue": "String1"
                   },
                   {
                       "myIntValue": 1
                   },
                   {
                       "myStringValue": "String2"
                   }
               ]
           }
       }
    }
    

    Lastly, you could just have multiple attributs, one for each type, such as follow:

    type Total {
        key : String
        valuesInt: [Int]
        valuesString: [String]
    }
    
    type Query {
        filter(key: String!) : Total
    }