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 TypeToken. The 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
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.