How a Little-Known Jackson Feature Helped Secure My Domain Code
Discover how the Mixin feature allows you to annotate your code at a distance.
Isolating the domain logic
A well-known concept in domain-driven design is that the domain code shouldn’t depend on external libraries or modules. The idea is widely recognized outside of the DDD community and is not limited to object-oriented programming.
However, some frameworks and libraries like Spring, Jackson, or JPA offer annotation-based usage/configuration in the Java world. Therefore, it is very tempting to use the annotations in the domain code to omit work that is typically boilerplate-like (creating wrappers, DTOs, custom de/serializers, etc.).
Recently, I faced this temptation while trying to manage the JSON serialization of the type hierarchy. Fortunately, I didn’t give in, and I found a neat solution to the problem I faced. Now I want to share my story with you.
I will use simplified yet equally powerful code in this article, so as not to share the original code publicly.
Example domain code
To begin, please look at the domain code below.
The Event interface is the base type for all the domain events, and there are two exemplary domain events.
The actual events should be stored in EventStore, like the one below.
Imagine you would like to persist events in JSON format and use one of the popular Java frameworks. Therefore you pick Jackson for event serialization as it seems well-suited to the task.
Problem: type hierarchy serialization
Handling type hierarchies while using the Jackson library is the user’s responsibility.
First, you choose the base type of the hierarchy, selecting Event interface, of course.
Second, you have to decide if metadata should be stored in serialized subtypes to enable deserialization to the expected type. There are a few options to consider here. To eliminate ambiguity and simplify this example, let’s define that the canonical class name will be added to the serialized event as a value of the @type property. Jackson provides an option for that.
So, the serialized SomethingHappenedEvent will look like this:
Jackson provides a simple way of doing this with annotations. Have a look at the Event interface now.
But wait. Something is wrong. The domain code has annotation now, and therefore there is a dependency between Jackson and the domain. In this case, other projects or modules that depend on the domain would also be dependent on Jackson. This is unacceptable.
Before I found my final solution, there were two earlier attempts that I’d like to share with you. Feel free to skip this section if you want.
Custom serialization and deserialization
It seems straightforward, right? It is easy to write a serializer. You just do the regular serialization, and then you add the extra property @type, and et voilà!
The problem starts with the deserializer. The @type property must be read first. Then, based on the value, the suitable class must be found in the classpath, and the deserializer must create an instance. To do that, you might even end up using a reflection mechanism. Nevertheless, it is tedious and redundant work. Just look at the annotation approach. Additionally, the Jackson API for writing such a deserializer is not very welcoming or pleasant to use.
Storing a canonical name is the most straightforward approach. Just think about what would happen if you used a more complex identifier. Actually, this was precisely the situation I was facing while discovering the solution to this problem.
Jackson provides a mechanism called default typing. It could be used here, but there are some issues. First, it needs to be set on the ObjectMapper level (the main class for handling serialization). To do that, you’ll first need a good understanding of Jackson. What’s more, it’s like shooting a fly with a cannon! Jackson’s default typing is incredibly complicated to set up as it was created for more complex uses than a one type-hierarchy. Doing that just feels wrong and, compared to @JsonTypeInfo, much harder.
A Holy Grail solution
I’ve presented two possible solutions, but they were pretty complex, especially when contrasted with using annotation directly in the domain code.
You may disagree with the whole annotation approach. You may think of it as magic happening behind the scenes. However, the fact it’s entirely painless to implement is beyond discussion.
So, what if I tell you there is a way to move the annotations out of the domain code? To keep it internal for serialization/Jackson-related code without polluting other parts of the system? Yeah, there is a way.
It is Jackson’s feature called Mixin.
Mixin is an object-oriented programming concept. It is a class with generic functionality that can be later “mixed” with other classes, hence the name. The focus here is on code reuse rather than inheritance.
Mixins aren’t new — they’ve been around for many years. For example, if you search the concept, Wikipedia states that Java’s default methods are mixins and many languages have the feature. Here, you can see an example found in Scala.
Applying the solution
The Mixin feature has relatively poor documentation. Unfortunately, the examples on the web all focus on including or excluding the fields of classes during serialization. However, the feature still works as a solution to the problem described in this article.
Jackson’s SimpleModule API has the method
public SimpleModule setMixInAnnotation(Class<?> targetType, Class<?> mixinClass)
Its JavaDoc explains it thus:
The method for specifying that annotations defined by mixinClass should be “mixed in” with annotations that targetType has (as if they were directly included on it!).
Mix-in annotations are registered when a module is registered for ObjectMapper.
Therefore, you need just two extra steps to achieve non-demanding serialization of the Event hierarchy. Firstly, a class like this is required:
Then you just need to do some configuration:
Finally, you should then see the green test results.
It’s worth mentioning that the classes above cannot be placed in different packages than the domain code. They can, for example, be placed in another Gradle module. An example of this approach can be seen in the article repo here.
We simply need to make the domain code available from the location where you set up the serialization for the approach to work.
At first, I assumed that there would be no annotation-based solution to the problem that my team and I would accept. But, as it turned out, I couldn’t be more wrong: a brilliant solution was there just waiting to be found.
The mixin approach proved nearly flawless (as it is still annotation-based). It prevents you from having to write boilerplate code by stopping you from placing annotations everywhere in your domain. It simply appears to be the best option in Jackson for serializing type hierarchies.
I hope that the article is helpful and will save you from spending three days looking for a solution to the problem, just like I did two weeks ago!
You can find the example code in the repository here.