After reading the comments and seeing how generic objects are deserialized, I think I can provide an answer that better addresses your problems:
- Deserializing using the
Map interface (from the comment section).
- Deserializing generic objects (from post).
- Sorting/retrieving elements with
String.CASE_INSENSITIVE_ORDER Comparator (from post).
- Maintaining
String.CASE_INSENSITIVE_ORDER Comparator (from comment section).
Deserializing using the Map interface
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.
Deserializing generic objects
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.
Sorting/retrieving elements with String.CASE_INSENSITIVE_ORDER Comparator
In 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.
Maintaining String.CASE_INSENSITIVE_ORDER Comparator
This 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);
Recap Example
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;
}
}
TreeMapwith Jackson'sObjectMapper, theTreeMap's elements are actually serialized in the correct order: onecompiler.com/java/426as79jd