Generic Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

Over the years, GraphQL has been widely accepted as one of the patterns of communication for web services. Though it's rich and flexible in use, it may pose challenges in some scenarios. One of them is to return a Map from a query, which is a challenge since a Map is not a type in GraphQL.

In this tutorial, we'll learn techniques to return a Map from a GraphQL query.

2. Example

Let's take the example of a product database having an indefinite number of custom attributes.

A Product, as a database entity, may have some fixed fields like name, price, category, etc. But, it may also have attributes that vary from category to category. These attributes should be returned to the client in a way that their identifying keys remain preserved.

For that purpose, we can make use of a Map as the type of these attributes.

3. Return Map

In order to return a Map, we've got three options:

  • Return as JSON String
  • Use GraphQL custom scalar type
  • Return as List of key-value pairs

For the first two options, we'll be making use of the following GraphQL query:

query {
    product(id:1){ 
        id 
        name 
        description 
        attributes 
    }
}

The parameter attributes will be represented in the Map format.

Next, let's look at all three options.

3.1. JSON String

This is the simplest option. We'll serialize the Map into JSON String format in the Product resolver:

String attributes = objectMapper.writeValueAsString(product.getAttributes());

The GraphQL schema itself is as follows:

type Product {
    id: ID
    name: String!
    description: String
    attributes:String
}

Here's the result of the query after this implementation:

{
  "data": {
    "product": {
      "id": "1",
      "name": "Product 1",
      "description": "Product 1 description",
      "attributes": "{\"size\": {
                                     \"name\": \"Large\",
                                     \"description\": \"This is custom attribute description\",
                                     \"unit\": \"This is custom attribute unit\"
                                    },
                   \"attribute_1\": {
                                     \"name\": \"Attribute1 name\",
                                     \"description\": \"This is custom attribute description\",
                                     \"unit\": \"This is custom attribute unit\"
                                    }
                        }"
    }
  }
}

This option has two issues. The first issue is that the JSON string needs to be processed on the client-side into a workable format. The second issue is that we can't have a sub-query on attributes.

To overcome the first issue, the second option of GraphQL custom scalar type can help.

3.2. GraphQL Custom Scalar Type

For the implementation, we'll make use of the Extended Scalars library for GraphQL in Java.

Firstly, we'll include the graphql-java-extended-scalars dependency in pom.xml:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-extended-scalars</artifactId>
    <version>2022-04-06T00-10-27-a70541e</version>
</dependency>

Then, we'll register the scalar type of our choice in the GraphQL configuration component. In this case, the scalar type is JSON:

@Bean
public GraphQLScalarType json() {
    return ExtendedScalars.Json;
}

Lastly, we'll update our GraphQL schema accordingly:

type Product {
    id: ID
    name: String!
    description: String
    attributes: JSON
}
scalar JSON

Here's the result after this implementation:

{
  "data": {
    "product": {
      "id": "1",
      "name": "Product 1",
      "description": "Product 1 description",
      "attributes": {
        "size": {
          "name": "Large",
          "description": "This is custom attribute description",
          "unit": "This is a custom attribute unit"
        },
        "attribute_1": {
          "name": "Attribute1 name",
          "description": "This is custom attribute description",
          "unit": "This is a custom attribute unit"
        }
      }
    }
  }
}

With this approach, we won't need to process the attributes map on the client-side. However, the scalar types come with their own limitations.

In GraphQL, scalar types are the leaves of the query, which suggests that they can't be queried further.

3.3. List of Key-Value Pairs

If the requirement is to query further into the Map, then this is the most feasible option. We'll transform the Map object into a list of key-value pair objects.

Here's our class representing a key-value pair:

public class AttributeKeyValueModel {
    private String key;
    private Attribute value;
    
    public AttributeKeyValueModel(String key, Attribute value) {
        this.key = key;
        this.value = value;
    }
}

In the Product resolver, we'll add the following implementation:

List<AttributeKeyValueModel> attributeModelList = new LinkedList<>();
product.getAttributes().forEach((key, val) -> attributeModelList.add(new AttributeKeyValueModel(key, val)));

Finally, we'll update the schema:

type Product {
    id: ID
    name: String!
    description: String
    attributes:[AttributeKeyValuePair]
}
type AttributeKeyValuePair {
    key:String
    value:Attribute
}
type Attribute {
    name:String
    description:String
    unit:String
}

Since we've updated the schema, we'll update the query as well:

query {
    product(id:1){ 
         id 
         name 
         description 
         attributes {
             key 
             value {
                 name 
                 description 
                 unit
             }
        } 
    }
}

Now, let's look at the result:

{
  "data": {
    "product": {
      "id": "1",
      "name": "Product 1",
      "description": "Product 1 description",
      "attributes": [
        {
          "key": "size",
          "value": {
            "name": "Large",
            "description": "This is custom attribute description",
            "unit": "This is custom attribute unit"
          }
        },
        {
          "key": "attribute_1",
          "value": {
            "name": "Attribute1 name",
            "description": "This is custom attribute description",
            "unit": "This is custom attribute unit"
          }
        }
      ]
    }
  }
}

This option also has two issues. The GraphQL query has become a bit complex. And the object structure needs to be hardcoded. Unknown Map objects won't work in this case.

4. Conclusion

In this article, we've looked into three different techniques to return a Map object from a GraphQL query. We discussed the limitations of each of them. Since none of the techniques is perfect, they must be used based on the requirements.

As always, the example code for this article is available over on GitHub.

Generic bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Generic footer banner
Comments are closed on this article!