Advanced JSON (de)serialization Using Java's Jackson

Jackson is an excellent library for serializing and deserializing JSON data in Java. It's a fairly quick and painless way to get the job done, until you need custom (de)serialization logic. If you get to that point, you'll instantly wish you were using some language that makes JSON work seamless--like Node.js--but that's not always an option, so the next best solution is to write yourself some custom (de)serialization functionality. I guess it's still substantially better than programming in assembly language...

TL;DR: There's working demonstration code here if you don't like reading.

Advanced (de)serialization

So you've decided, or more likely someone has decided for you, that they just don't want to make your life easy. In making this decision, they've come up with some evil and convoluted JSON data format that you'll need to support. After taking a moment to get angry, followed by many deep breaths and curse words, you decide that you might as well get to it and write a custom (de)serializer. It's not that hard once you understand the code. I've done the painful part for you...

First, let's assume for some crazy reason that your JSON looks like this:

{
  "cars": {
    "japanese": {
      "makes": {
        "honda": {
          "models": {
            "civic": {
              "trim": "si",
              "engineSize": "2.0L",
              "tireSize": "215/45/17",
              "vin": "234HY345858BH9342437",
              "price": 15000,
              "mileage": 30000
            }
          }
        }
      }
    }
  }
}

After you get over the shock of it, you'll realize that this is a pretty horribly structured JSON. Oh well, it happens. What do you do now? Well, you have two options.

  1. You can write a ton of nested Java objects that are very brittle. Nah.
  2. You can just skip over all the useless noise and (de)serialize the only part you care about...The relevant data. Better. Not perfect, but better.

How?

Once you slog through the Jackson documentation for some time, you might figure some stuff out. Ah, whatever, I'll just tell you to save you some time. The key to custom (de)serialization involves a handful of classes, so let's get to it.

The Java Object

Since we only care about the data nested on the inner-most object of the JSON, we'll create a Java Object that contains only that data.

public class NestedCarModel {

    @JsonProperty("trim")
    private String trim;

    @JsonProperty("engineSize")
    private String engineSize;

    @JsonProperty("tireSize")
    private String tireSize;

    @JsonProperty("vin")
    private String vin;

    @JsonProperty("price")
    private Integer price;

    @JsonProperty("mileage")
    private Integer mileage;

    // Getters, setters, hashcode, equals, etc.
}

Deserialization

Deserialization is the process of taking a JSON string and converting it into a Java Object that represents the string.

Custom Deserialization Code

First, we'll write the custom deserializer logic. Our deserializer will implement the ResolvableDeserializer interface and extend the StdDeserializer class.

public class NestedCarModelDeserializer extends StdDeserializer<NestedCarModel> implements ResolvableDeserializer  

At a high level, our deserializer will walk down the JSON tree of objects that we don't care about, "cars", "japanese", "makes", "honda", "models", "civic", to get to the data that we actually care about. We will do that by iterating over each tree node, where the names of the nodes are defined in the modelTraversalPath String[]:

for(String nodePath : modelTraversalPath) {  
    node = node.findValue(nodePath);
    if(node == null) {
        throw new IllegalStateException("Unexpected json traversal path format!");
    }
}

Once we've walked down the JSON tree objects, we'll create a new JsonParser (which represents the JSON to be parsed) that only contains the data for deserialization.

String treeString = objectMapper.writeValueAsString(objectMapper.treeToValue(node, Object.class));  
JsonParser newParser = jsonFactory.createParser(treeString);  
newParser.nextToken();  

Lastly, we will delegate the the deserialization of the data to Jackson since, after all, we don't really want to be doing the nitty-gritty deserialization:

return (NestedCarModel) defaultJsonDeserializer.deserialize(newParser, deserializationContext);  

Notice how our implementation uses the defaultJsonDeserializer, which is simply a JsonDeserializer instance. We'll pass this instance to our custom deserializer in a later step.

Viola! You've fairly painlessly deserialized your ugly JSON. Oh wait, there's more...

Registering the Custom Deserializer

Now that you've created your custom deserializer, you need to register it with Jackson so it will be used to deserialize instances of your NestedCarModel. This is where the convoluted code rears its ugly head.

First, you need to create class that extends the BeanDeserializerModifier. Next you need to override the #modifyDeserializer() function to add custom logic. In this function, you need to check the type of the object being deserialized. If it's the type you are interested in deserializing--in this case, the NestedCarModel--then you'll return your NestedCarModelDeserializer. Otherwise, you'll let Jackson do its thing...

private final class DeserializerModifier extends BeanDeserializerModifier {

    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        if (NestedCarModel.class.equals(beanDesc.getBeanClass())) {
            return new NestedCarModelDeserializer(deserializer, objectMapper, modelTraversalPath);
        }

        return super.modifyDeserializer(config, beanDesc, deserializer);
    }

}

Next, you'll create a class that extends the Jackson SimpleModule, which is used to register our DeserializerModifier.

private static final class DeserializerModule extends SimpleModule {

    private BeanDeserializerModifier deserializerModifier;

    public DeserializerModule(BeanDeserializerModifier deserializerModifier) {
        super("DeserializerModule", Version.unknownVersion());
        this.deserializerModifier = deserializerModifier;
    }

    @Override
    public void setupModule(SetupContext context) {
        super.setupModule(context);
        context.addBeanDeserializerModifier(deserializerModifier);
    }

}

Thank goodness this is almost over, the code's starting to get ugly. Lastly, you need to register the SimpleModule with ObjectMapper that you will use to actually (de)serialize your data:

ObjectMapper objectMapper = new ObjectMapper();  
objectMapper.registerModule(new DeserializerModule(new DeserializerModifier()));  

NOW you can actually deserialize your JSON data to a Java Object:

public NestedCarModel deserialize(String nestedCarModelJson) throws IOException {  
    return objectMapper.readValue(nestedCarModelJson, NestedCarModel.class);
}

Whew, that was a bit more work. Now you just need to serialize your Object to JSON. I swear there's a light at the end of the tunnel...

Serialization

Serialization is the process of taking a Java Object and converting the data in the object into a JSON string.

Custom Serialization Code

Again, we'll start with the custom logic to serialize the Object. The serializer will implement the ResolvableSerializer interface and extend the StdSerializer class.

public class NestedCarModelSerializer extends StdSerializer implements ResolvableSerializer  

First, we will serialize the Object to JSON:

JsonGenerator tempGenerator = jsonFactory.createGenerator(new ByteArrayOutputStream());

// Use the default Jackson serializer
defaultJsonSerializer.serialize(nestedCarModel, tempGenerator, serializerProvider);  
// Flush the serialized string to the stream so it can be retrieved
tempGenerator.flush();  

Notice how we are delegating the hardest part, the serialization of our NestedCarModel object, to Jackson. Why reinvent the wheel? Especially when someone else does the hard work for us...

Next, we will construct all of JSON objects that we don't care about, the "cars", "japanese", "makes", "honda", "models", "civic" objects.

// Create the root object
jsonGenerator.writeStartObject();  
jsonGenerator.flush();  
for (String path : modelTraversalPath) {  
    // Write each object path to the final generator
    jsonGenerator.writeObjectFieldStart(path);
}

Lastly, we'll nest the serialized NestedCarModel inside of our JSON objects and then close all of the objects so we have well-formed JSON:

ByteArrayOutputStream baos = (ByteArrayOutputStream) tempGenerator.getOutputTarget();  
String generatedJson = baos.toString();  
// Strip the root objects out of the json generated by default serializer
generatedJson = generatedJson.substring(1, generatedJson.length() - 1);  
tempGenerator.close();

// Write the raw data generated by the default serializer to the JsonGenerator
jsonGenerator.writeRaw(generatedJson);

for (int i = 0; i < modelTraversalPath.length; i++) {  
    // Close each object path on the final generator
    jsonGenerator.writeEndObject();
}

// Close the root object
jsonGenerator.writeEndObject();  
jsonGenerator.flush();  

Awesome! We've now got a serializer that's producing the JSON String that we initially consumed with the deserializer. This makes the serialization and deserialization process symmetrical. More on that later.

Now all we have to do is all that registration crap again...

Registering the Custom Serializer

Once again, you need to register you custom serializer with Jackson so it will be used to serialize instances of your NestedCarModel. Time for that annoying code again...

Create class that extends the BeanSerializerModifier. Then you override the #modifySerializer() function to add custom logic. You'll need to check the type of the object being serialized and take the appropriate action. If it's what you want to serialize--obviously the NestedCarModel--then you'll return your NestedCarModelSerializer. Otherwise, let Jackson work its magic without your help...

private final class SerializerModifier extends BeanSerializerModifier {

    @Override
    public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
        if (NestedCarModel.class.equals(beanDesc.getBeanClass())) {
            return new NestedCarModelSerializer(jsonFactory, (JsonSerializer<Object>) serializer, modelTraversalPath);
        }

        return super.modifySerializer(config, beanDesc, serializer);
    }

}

Once again, you'll create yet another class that extends the Jackson SimpleModule, which is used to register our SerializerModifier.

private static final class SerializerModule extends SimpleModule {

    private BeanSerializerModifier serializerModifier;

    public SerializerModule(BeanSerializerModifier serializerModifier) {
        super("SerializerModule", Version.unknownVersion());
        this.serializerModifier = serializerModifier;
    }

    @Override
    public void setupModule(SetupContext context) {
        super.setupModule(context);
        context.addBeanSerializerModifier(serializerModifier);
    }

}

We are so close to having something awesome. The last step is to register the SimpleModule with ObjectMapper so your data will be serialized properly:

ObjectMapper objectMapper = new ObjectMapper();  
objectMapper.registerModule(new DeserializerModule(new DeserializerModifier()));  
objectMapper.registerModule(new SerializerModule(new SerializerModifier()));  

NOW you can serialize your JSON data to a Java Object:

public String serialize(NestedCarModel nestedCarModel) throws IOException {  
    return objectMapper.writeValueAsString(nestedCarModel);
}

Whew, what a task...Aren't you glad I did all the hard work for you on that?! Now, for some closing thoughts...

Symmetry

You should be aiming for symmetry across your serialization and deserialization. Put another way:

(JavaObject --> JsonString) <----> (JsonString --> JavaObject)

You should always be able to go seamlessly from the JavaObject state, to the JsonString state, and then back to the JavaObject state--and visa-versa--without the represented state of the data being changed. Not achieving symmetry can lead to some nasty unpredictable bugs for you, or even worse, the next helpless victim who has to maintain your code. You wouldn't want your name on bad code now, would you?

Conclusion

As I said before, the Jackson library is quite powerful and quite useful--that is of course, if you know what you're doing. Hopefully I can save someone a few hours to a few days of pulling there hair out. If not, sorry, I gave it my best shot.