Case insensitive deserialization
Growing up in Windows with BASIC you learn case doesn't matter, so Color
is the same as COLOR
or cOLOR
when it comes to variable names. Same applies to @Formula
or item names in Notes documents.
On the other side, Linux, Java, JavaScript and JSON are very much case sensitive.
This poses a challenge when deserializing (handcrafted) JSON files.
The Task at hand
Deserialization of JSON into a Java class instance can be done using jackson. This is also what the JsonObject in vert.x uses when you call json.mapTo(SomeClass)
. Not using vert.x? You can use the ObjectMapper
. Let's look at a sample Java class
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.vertx.core.json.JsonObject;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class JsonTest {
public static fromJson(final JsonObject source) {
return source.mapTo(JsonTest.class);
}
private String color;
private String shape;
private int answer;
private boolean pretty;
/* GETTERS and SETTERS omitted for brevity
Let your IDE add them for you */
}
Now you want to deserialize a good JSON, which works as expected:
{
"color": "Red",
"shape": "round",
"answer": 11,
"pretty": true
}
but the very moment your JSON isn't following proper capitalization, like human provided JSON,
{
"Color": "Red",
"Shape": "square",
"Answer": 42,
"pretty": true,
"ignore": "this"
}
deserialization will fail. We need to fix that.
Introspection
All Java snippets are located as static functions in a helper class JsonHelper
.
As first operation we need to obtain a list of properties our Java class expects when instantiated by Jackson:
public static List<String> getClassFields(final Class<?> clazz) {
final ObjectMapper mapper = new ObjectMapper();
final JavaType javaType = mapper.getTypeFactory().constructType(clazz);
return mapper.getSerializationConfig()
.introspect(javaType)
.findProperties()
.stream()
.map(BeanPropertyDefinition::getName)
.collect(Collectors.toList());
}
Then we turn the list into an case insensitive map:
public static Map<String, String> listToCaseInsensitiveMap(final List<String> source) {
final TreeMap<String, String> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
source.stream()
.filter(Objects::nonNull)
.forEach(s -> result.put(s, s));
return result;
}
This finally can be used to update the JsonObject to match the expected capitalization:
public static JsonObject normalizeAndSelect(final JsonObject source, final Map<String, String> fields) {
final JsonObject result = new JsonObject();
source.stream()
.filter(entry -> fields.containsKey(entry.getKey()))
.forEach(entry -> result.put(fields.get(entry.getKey()), entry.getValue()));
return result;
}
// Facade function to tie it all up
public static JsonObject normalizeAndSelect(final JsonObject source, Class<?> clazz) {
return normalizeAndSelect(source, listToCaseInsensitiveMap(getClassFields(clazz)));
}
// Aternate facade
public static Object normalizeAndReturn(final JsonObject source, Class<?> clazz) {
return normalizeAndSelect(source, listToCaseInsensitiveMap(getClassFields(clazz)))
.mapTo(clazz);
}
This finally lets us update the fromJson
static method in our POJO class:
public static fromJson(final JsonObject source) {
return JsonHelper.normalizeAndSelect(source,JsonTest.class)
.mapTo(JsonTest.class);
}
//Alternate version
public static fromJson2(final JsonObject source) {
return (JsonTest) JsonHelper.normalizeAndReturn(source,JsonTest.class);
}
As usual: YMMV
Posted by Stephan H Wissel on 08 June 2022 | Comments (1) | categories: Java vert.x