wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

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 on 08 June 2022 | Comments (1) | categories: Java vert.x

Comments

  1. posted by Ben Langhinrichs on Saturday 11 June 2022 AD:

    That looks quite clever. Thanks.