Dealing with Value Objects in REST API with Jackson

Dealing with serialization and deserialization of ValueObjects with Jackson seems to be straightforward, right? It’s very common to expose REST API in our applications and JSON format is one of the most frequently used ones. How do we usually do it?

Data Transfer Objects

One of the most popular approaches is to introduce a set of Data Transfer Objects, which define external messages format. Let’s have a look at an example.

This example is very simple, but even now we have to transform ProjectId to String during serialization and String to ProjectId during deserialization. What will happen when Value Object is more complex and contains 3-4 fields? and one of the fields is another Value Object?

In case of Address, dealing with serialization and deserialization Address to/from AddressDto is not a very pleasant task to do. It is not just a matter of adding AddressDto in each resource containing Address, but also transforming the values.

Each time you want to compose Address in your Data Transfer Objects (such as CustomerDto in the example above) you have to prepare a data structure ( AddressDto in this case) and the transformation. Of course it can be reused in a single module, but not in case of a multi-module complex application — usually the DTO/converter code is duplicated.

There are tools to make the mapping and transformation easier, but anyway:

  1. You have to do it (in the code, tool configuration, etc.)
  2. You have to test it, because it is very probable that you will forget to pass some value.

Why can not we use the Value Object directly and serialize/deserialize its instances?

Please take a look at the full example of Address value object again. What will happen when you try to deserialize the object from JSON string?

Jackson annotations

You can easily fix it by adding some Jackson annotations which allow to define a way how the object should be constructed:

Now Address can be deserialized from JSON and everything works fine but…

We have just added some additional dependency to Jackson, which which sometimes may not be an acceptable approach. In case of a project where Address is used internally in a single module, it works perfectly fine and we can live with this Jackson dependency.

It becomes more of a challenge if we expose a set of interfaces and value objects to the external world (e.g. as a library). We should avoid forcing users to include Jackson in their project just because it was convenient for our internal use.

Is there any other way to deal with simple Value Objects?

Custom serializer / deserializer

I think we should deal with this situation in exactly the same way as we deal with Java 8 date/time classes. We can prepare set of custom serializers and deserializers.

At first glance it looks like a non-trivial task to deal with low-level Jackson API. Actually it is much simpler than you might think. The main benefit of this approach is that you code and test it once and then you can reuse it whenever you want.

For simple Value Objects which are just wrappers for string values, we can prepare a common serializer and deserializer:

The only thing we have to do is to register it properly in a custom Jackson module:

Comparison of approaches

Thus far, we considered three approaches to serialization of value objects:

  1. Mapping them to and from DTOs
  2. Annotating value objects directly
  3. Writing custom serializers and deserializers and delivering them in a Jackson module

I summarized pros and cons of different approaches in the table below.

Pros Cons
DTOs
  • easy and straight forward for simple classes
  • easy to check the JSON format just by looking at the source code
  • can cause duplication of data structures when Value Object is required to be used in more than one application or module

  • requires additional tests to check if serialization is correct in each module separately

  • you can write a common DTO/VO converter and share it with other modules, but the API users have to be aware of the conversion and explicitly use it whenever they want to serialize or deserialize messages
Jackson annotations directly on Value Objects
  • easy to use
  • allows to reuse mapping between internal modules
  • requires additional dependency to Jackson annotations – not acceptable when exposed to external systems
Custom serializer / deserializer
  • easy to reuse between modules – write & test once, use everywhere
  • easy to distribute as an additional Jackson module
  • requires low level implementation (but it is much simpler than expected)
  • harder to define required/optional fields

Recommended scenarios

While a custom Jackson module is a great solution for some cases, there are cases where the traditional DTO-based model works perfectly fine. Based on the pros and cons listed above, I think that we can define the target scenario for each approach:

Recommended usage scenario
DTOs Having Jackson annotations on value objects is not an option. Value Object is used in single module – it’s easy to reuse data structures and transformations
Jackson annotations directly on Value Objects Value Objects are used in multiple modules internally and Jackson dependency is not a problem (e.g. when target module uses Jackson anyway)
Custom serializer / deserializer Having Jackson annotations on value objects is not an option. Value Object is reused in many modules or external applications. It is worth to spend a bit more effort to prepare a serializer / deserializer once and reuse it

Summary

As we can see, a silver bullet solution does not exist, even for a problem as simple as value object serialization. There are consequences of each design decision we make. They are very important especially when there are external consumers of our classes. Adding an annotation on our class might seem like a good idea, but it can also mean that somebody has to include a Jackson jar in their project. It is always a worthwhile experience to assess our decisions by placing ourselves in our users’ shoes. I encourage you to do so on daily basis!

Leave a Reply

Your email address will not be published. Required fields are marked *