We've opened a new role Backend Java/Spring Team Lead with Integration Experience. Part-time and entirely remote, of course.
Return Map from GraphQL
Last updated: January 8, 2024
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.