Course – LS (cat=JSON/Jackson)

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

>> CHECK OUT THE COURSE

1. Introduction

In this quick tutorial, we’ll learn how to convert a JSON string to a Map using Gson from Google.

We’ll see three different approaches to accomplish that and discuss their pros and cons – with some practical examples.

2. Passing Map.class

In general, Gson provides the following API in its Gson class to convert a JSON string to an object:

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException;

From the signature, it’s very clear that the second parameter is the class of the object which we intend the JSON to parse into. In our case, it should be Map.class:

String jsonString = "{'employee.name':'Bob','employee.salary':10000}";
Gson gson = new Gson();
Map map = gson.fromJson(jsonString, Map.class);
Assert.assertEquals(2, map.size());
Assert.assertEquals(Double.class, map.get("employee.salary").getClass());

This approach will make its best guess regarding the value type for each property.

For example, numbers will be coerced into Doubles, true and false into Boolean, and objects into LinkedTreeMaps.

If there are duplicate keys, though, coercion will fail and it will throw a JsonSyntaxException.

And, due to type erasure, we won’t be able to configure this coercion behavior either. So, if we need to specify the key or value types, then we’ll need a different approach.

3. Using TypeToken

To overcome the problem of type-erasure for the generic types, Gson has an overloaded version of the API:

public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException;

We can construct a Map with its type parameters using Gson’s TypeTokenThe TypeToken class returns an instance of ParameterizedTypeImpl that preserves the type of the key and value even at runtime:

String jsonString = "{'Bob' : {'name': 'Bob Willis'},"
  + "'Jenny' : {'name': 'Jenny McCarthy'}, "
  + "'Steve' : {'name': 'Steven Waugh'}}";
Gson gson = new Gson();
Type empMapType = new TypeToken<Map<String, Employee>>() {}.getType();
Map<String, Employee> nameEmployeeMap = gson.fromJson(jsonString, empMapType);
Assert.assertEquals(3, nameEmployeeMap.size());
Assert.assertEquals(Employee.class, nameEmployeeMap.get("Bob").getClass());

Now, if we construct our Map type as Map<String, Object>, then the parser will still default as we saw in the previous section.

Of course, this still falls back to Gson for coercing primitive types. Those, however, can be customized, too.

4. Using Custom JsonDeserializer

When we need fine-grained control over the construction of our Map object, we can implement a custom deserializer of type JsonDeserializer<Map>.

To see an example, let’s assume our JSON contains the employee’s name as key and their hire date as its value. Further, let’s assume the date’s format is yyyy/MM/dd, which is not a standard format for Gson.

We can configure Gson to parse our map differently, then, by implementing a JsonDeserializer:

public class StringDateMapDeserializer implements JsonDeserializer<Map<String, Date>> {

    private SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd");

    @Override
    public Map<String, Date> deserialize(JsonElement elem,
          Type type,
          JsonDeserializationContext jsonDeserializationContext) {
        return elem.getAsJsonObject()
          .entrySet()
          .stream()
          .filter(e -> e.getValue().isJsonPrimitive())
          .filter(e -> e.getValue().getAsJsonPrimitive().isString())
          .collect(
            Collectors.toMap(
              Map.Entry::getKey,
              e -> formatDate(e.getValue())));
    }

    private Date formatDate(Object value) {
        try {
            return format(value.getAsString());
        } catch (ParseException ex) {
            throw new JsonParseException(ex);
        }
    }
}

Now, we have to register it in the GsonBuilder against our target type Map<String, Date> and build a customized Gson object.

When we call the fromJson API on this Gson object, the parser invokes the custom deserializer and returns the desired Map instance:

String jsonString = "{'Bob': '2017-06-01', 'Jennie':'2015-01-03'}";
Type type = new TypeToken<Map<String, Date>>(){}.getType();
Gson gson = new GsonBuilder()
  .registerTypeAdapter(type, new StringDateMapDeserializer())
  .create();
Map<String, Date> empJoiningDateMap = gson.fromJson(jsonString, type);
Assert.assertEquals(2, empJoiningDateMap.size());
Assert.assertEquals(Date.class, empJoiningDateMap.get("Bob").getClass());

This tactic is also useful when our map may contain heterogeneous values and we have a fair idea of how many different types of values could be there.

To learn more about a custom deserializer in Gson, feel free to go through the Gson Deserialization Cookbook.

5. Conclusion

In this short article, we learned several ways to construct a map from a JSON-formatted string. And we also discussed proper use-cases for these variations.

The source code for the examples is available over on GitHub.

Course – LS (cat=JSON/Jackson)

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

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.