I am trying to use Jackson's ObjectMapper
class to serialize an object that looks like this:
TreeMap<String, String> mappings = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
but when the object is serialized, it ends up looking like this:
{"mappings": {"key": "value"}}
When deserializing, it loses the case insensitive property of the map. Does anyone know how to resolve this, or possibly a type of case insensitive map class which I can use to serialize and deserialize? Is there a Jackson mapper property that I can use to fix this issue?
Here is some sample code:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) throws JsonProcessingException {
TreeMap<String, String> mappings = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
mappings.put("Test3", "3");
mappings.put("test1", "1");
mappings.put("Test2", "2");
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(mappings);
System.out.println(json);
TreeMap<String, String> deserMappings = objectMapper.readValue(json, TreeMap.class);
System.out.println("Deserialized map case insensitive test: " + deserMappings.get("test3"));
}
}
And sample output:
{
"test1" : "1",
"Test2" : "2",
"Test3" : "3"
}
Deserialized map case insensitive test: null
After reading the comments and seeing how generic objects are deserialized, I think I can provide an answer that better addresses your problems:
Map
interface (from the comment section).String.CASE_INSENSITIVE_ORDER
Comparator (from post).String.CASE_INSENSITIVE_ORDER
Comparator (from comment section).When deserializing a Map
, the default implementation used by Jackson is a HashMap
, if no actual implementation is specified. This is because an interface is not a concrete class, and Jackson needs an actual Map
implementation to know how to store the deserialized values. This is what was happening to you, judging from the initial problem described in the comments:
I originally had this deserializing to a field which was Map<String, String> but when I checked if it was a TreeMap using mappings instanceof TreeMap it returned false. It returned true when I did mappings instanceof HashMap.
When deserializing generic objects, you should use an instance of the abstract class TypeReference
that subclasses the generic type you're trying to read. In your case: new TypeReference<TreeMap<String, String>>() {}
. Like so, the argument type of the TypeReference
(TreeMap<String, String>
) is used by Jackson to reconstruct the Json values (test1
, 1
, Test2
, 2
, ...) into the argument types (String
and String
) of the generic type subclassed by the TypeReference
. In your snippet, the code is still working only coincidentally, because in the absence of a TypeReference
, Jackson reads simple values as String
, while complex values (objects) as LinkedHashMap
. Using a TypeReference
is fundamental when deserializing generic types.
String.CASE_INSENSITIVE_ORDER
ComparatorIn the code deserMappings.get("test3")
, I'm assuming, but I'm not sure, that there might be a misunderstanding in the way you expect the Comparator
to work. The Comparator String.CASE_INSENSITIVE_ORDER
supplied to the TreeMap
's constructor is only used to establish an ordering between the elements. It is not used to treat those elements in a case-insensitive way. Therefore, when you try to retrieve an element with the key test3
, nothing is returned because there is no value identified by test3
, but there is by Test3
. The Comparator
doesn't perform a comparison when retrieving elements, but only when storing them.
String.CASE_INSENSITIVE_ORDER
ComparatorThis is related to the comment:
My main issue is the issue with deserializing not maintaining the case insensitive property
The reason why the TreeMap
read with the ObjectMapper
is not maintaining the original case-insensitive order is because you're creating a whole new instance with a default order, which, in the case of String
keys, is a case sensitive ordering. In your code, at no point you've defined a custom comparator for the second TreeMap
instance (deserMappings
).
TreeMap<String, String> deserMappings = objectMapper.readValue(json, TreeMap.class);
Here, I'm sharing an example and a demo at OneCompiler.com that bind together the previous points:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) throws JsonProcessingException {
//Serializing a TreeMap with String keys and String values
TreeMap<String, String> mappings = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
mappings.put("Test3", "3");
mappings.put("test1", "1");
mappings.put("Test2", "2");
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(mappings);
System.out.println("mappings: \n" + json);
// Deserializing to a generic type without TypeReference (wrong approach).
//
// First of all, a small side note on deserializing with an interface. When you don't use a specific implementation
// but an interface when deserializing Maps, the default implementation used by Jackson is a HashMap. This is because
// an interface is not a concrete class, while Jackson needs an actual Map implementation to deserialize the Json
// values somewhere. This is what was happening to you, judging from the initial problem described in the comments:
//
// > "I originally had this deserializing to a field which was Map<String, String> but when I checked if it was a
// > TreeMap using mappings instanceof TreeMap it returned false. It returned true when I did mappings instanceof HashMap."
//
// Now, back to the TypeReference approach. In this case, even without using a TypeReference instance, the
// deserialization happens successfully because json values are read as String, which is coincidentally the same
// type of your Map's values. However, in general, when deserializing generic types you should subclass the actual
// type representing your data. This is because the argument type tells Jackson how to unmarshall those values.
Map<String, String> mappingsAsHashMap = objectMapper.readValue(json, Map.class);
System.out.println(mappingsAsHashMap instanceof TreeMap<String, String>); //Prints false because, by default, it was deserialized with a HashMap
Map<String, String> mappinggsAsTreeMap = objectMapper.readValue(json, TreeMap.class);
System.out.println(mappinggsAsTreeMap instanceof TreeMap<String, String>); //Prints true because you specified the actual implementation
//Works because the values have been read as a String
Map<String, String> mappingsWithStringValues = objectMapper.readValue(json, TreeMap.class);
String s = mappingsWithStringValues.get("test1");
System.out.println("length of s :" + s.length());
//Fails because tries to treat the values as Integer while the actual instances are String
Map<String, Integer> mappingsWithIntValues = objectMapper.readValue(json, TreeMap.class);
try {
Integer i = mappingsWithIntValues.get("test1");
System.out.println(i.intValue());
} catch (ClassCastException ex){
System.out.println("Attempting to read a String as an Integer");
}
//Serializing a TreeMap with String keys and a custom Bean
TreeMap<String, MyBean> mappingsBean = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
mappingsBean.put("Test3", new MyBean(3, "MyBean3"));
mappingsBean.put("test1", new MyBean(1, "MyBean1"));
mappingsBean.put("Test2", new MyBean(2, "MyBean2"));
String jsonBean = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(mappingsBean);
System.out.println("\n" + "mappingsBean:\n" + jsonBean);
// Deserializing the mappingsBean json without a TypeReference (wrong approach).
//
// Like so, Jackson doesn't know to which data type those inner values ({"id" : 2,"name" : "MyBean2"}) correspond to,
// therefore that sequence of key-value pairs is read as a LinkedHashMap, and even worse, it is added as such within
// your Map despite being a TreeMap<String, MyBean>! In fact, if you attempt to read or perform any operation on
// its values, you will raise a ClassCastException as the type expected for your values is MyBean and not LinkedHashMap.
mappingsBean = objectMapper.readValue(jsonBean, TreeMap.class);
try {
MyBean myBean = mappingsBean.get("test1");
} catch (ClassCastException ex) {
System.out.println("Attempting to read a LinkedHashMap as a MyBean");
}
try {
System.out.println(mappingsBean.get("test1").getId());
} catch (ClassCastException ex) {
System.out.println("Attempting to perform a MyBean's operation on a LinkedHashMap");
}
// Deserializing the mappingsBean json with a TypeReference (right approach).
//
// With this approach, Jackson knows via the argument type of the TypeReference, that those inner values
// ({"id" : 2,"name" : "MyBean2"}) correspond to a MyBean that needs to be reconstructed.
mappingsBean = objectMapper.readValue(jsonBean, new TypeReference<TreeMap<String, MyBean>>() {
});
System.out.println("\n" + mappingsBean);
//Proper deserialization of your original Map while maintaining the case insensitive order.
//
// If you attempt to simply read the initial json with a TypeReference that subclasses a TreeMap<String, String>
// you'll find out that it won't maintain your custom ordering, since the default ordering is case-sensitive.
// This is noticeable when looking at the print of mappingsBean, it follows the default case-sensitive ordering.
// Therefore, if you want to read your json as a TreeMap and maintain your custom case insensitive ordering, you
// need to define a temporary TreeMap where to read the json, and a result TreeMap initialized with the custom
// comparator and all the key-value pairs from the temporary TreeMap.
//Wrong ordering mappings
System.out.println("\nWrong Ordering mappings:\n" + objectMapper.readValue(json, new TypeReference<TreeMap<String, String>>() {
}));
//Right ordering mappings
TreeMap<String, String> mappingsTemp = objectMapper.readValue(json, new TypeReference<TreeMap<String, String>>() {
});
TreeMap<String, String> mappingsRes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
mappingsRes.putAll(mappingsTemp);
System.out.println("\nRight Ordering mappings:\n" + mappingsRes);
}
@NoArgsConstructor
@AllArgsConstructor
@Data
static class MyBean {
private int id;
private String name;
}
}