#protocols #java #jackson #messaging #design
Jackson mix-ins are crucial to clearly separate application layers according to the onion architecture (popularised further by Rob Martin), without code duplication and boilerplate. According to onion architecture marshalling would sit in infrastructure level and domain objects should not be dependent on messaging infrastructure.
Another important use case would be if you do not control source code for serialised objects, but rather import them as library. In this case it is impossible to annotate imported class.
Source code of examples can be found here.
Example that would just work™
Let’s say our library/domain class looks like this:
public class JacksonFriendlyZoo {
private int giraffeCount;
private boolean open;
public JacksonFriendlyZoo() {
// for deserialization, don't use me!
}
public JacksonFriendlyZoo(int giraffeCount, boolean open) {
this.giraffeCount = giraffeCount;
this.open = open;
}
public int getGiraffeCount() {
return giraffeCount;
}
public boolean isOpen() {
return open;
}
}
Then both (de-)serialization would just work with Jackson. However we should not alter domain object to make infrastructure work. So let’s see how to work around common cases below.
Immutable objects
Let’s start by removing no arg constructor because it leaks deserialization concern into domain code:
public class ImmutableZoo {
private final int giraffeCount;
private final boolean open;
public ImmutableZoo(int giraffeCount, boolean open) {
this.giraffeCount = giraffeCount;
this.open = open;
}
public int getGiraffeCount() {
return giraffeCount;
}
public boolean isOpen() {
return open;
}
}
This serializes well, but attempting to deserialize this would cause:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.romanmarkunas.blog.jackson.ImmutableZoo` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"giraffeCount":5,"open":true}"; line: 1, column: 2]
Let’s add a creator constructor using mix-in:
public abstract class ImmutableZooMixIn {
@JsonCreator
public ImmutableZooMixIn(
@JsonProperty("giraffeCount") int giraffeCount,
@JsonProperty("open") boolean open
) {}
}
And configure object mapper like so:
@Test
void canDeserializeWithoutDefaultConstructorButWithMixIn() {
ObjectMapper objectMapper
= new ObjectMapper()
.addMixIn(ImmutableZoo.class, ImmutableZooMixIn.class);
String jsonIn = "{\"giraffeCount\":5,\"open\":true}";
ImmutableZoo zoo = objectMapper.readValue(jsonIn, ImmutableZoo.class);
}
Improving code navigatability
Now you, like me, might be tempted to use creator factory method to leverage IDE code navigation features and have easier time to find out all places where object might be constructed, like so:
public abstract class ImmutableZooMixInFail {
@JsonCreator
public static ImmutableZoo create(
@JsonProperty("giraffeCount") int giraffeCount,
@JsonProperty("open") boolean open
) {
return new ImmutableZoo(giraffeCount, open);
}
}
however this would still fail with:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.romanmarkunas.blog.jackson.ImmutableZoo` (no Creators, like default constructor, exist)... blah, blah
This uncovers one important insight on how mix-ins work in Jackson - it does not look at mix-in class to find creator, but rather it sees the target class with added corresponding annotations from mix-in class. In other words, if target class does not have static factory method with exactly same signature as mix-in’s annotated static factory, the mix-in would not have any effect.
That means, if we don’t have factory on domain/library object, we can make code more navigatable only for non-final classes:
public abstract class ImmutableZooMixInNavigatable extends ImmutableZoo {
@JsonCreator
public ImmutableZooMixInNavigatable(
@JsonProperty("giraffeCount") int giraffeCount,
@JsonProperty("open") boolean open
) {
super(giraffeCount, open);
}
}
In any case the link between target and mix-in classes can be discovered when checking class usage and finding ObjectMapper configuration.
Methods not following Java property convention
By default Jackson looks for methods in the form of Java property to serialize objects.
For example, to serialize field giraffeCount
you must have method getGiraffeCount()
. So if our domain/library class looks like this:
public class NotFollowingJavaPropertyConventionZoo {
private final int giraffeCount;
private final boolean open;
public NotFollowingJavaPropertyConventionZoo(
int giraffeCount,
boolean open
) {
this.giraffeCount = giraffeCount;
this.open = open;
}
public int giraffeCount() {
return giraffeCount;
}
public boolean open() {
return open;
}
}
it will fail to serialize with:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.romanmarkunas.blog.jackson.NotFollowingJavaPropertyConventionZoo and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
This could be solved with mapper configuration:
new ObjectMapper().setVisibility(
PropertyAccessor.FIELD,
JsonAutoDetect.Visibility.ANY
);
However this would alter serialization for all classes, in potentially breaking way. This can be solved on a per-class basis using mix-ins:
public abstract class NotFollowingJavaPropertyConventionZooMixIn {
@JsonProperty("giraffeCount")
public abstract int giraffeCount();
@JsonProperty("open")
public abstract boolean open();
}
ObjectMapper objectMapper = new ObjectMapper().addMixIn(
NotFollowingJavaPropertyConventionZoo.class,
NotFollowingJavaPropertyConventionZooMixIn.class
);
NotFollowingJavaPropertyConventionZoo zoo
= new NotFollowingJavaPropertyConventionZoo(42, false);
objectMapper.writeValueAsString(zoo);
Fields that should not be deserialized
Another interesting example would be an object that has some properties that we don’t want to serialize, for example, a cached field:
public class HasPropertiesToIgnoreZoo {
private final int giraffeCount;
private final int sealCount;
private final int totalCountCache;
public HasPropertiesToIgnoreZoo(int giraffeCount, int sealCount) {
this.giraffeCount = giraffeCount;
this.sealCount = sealCount;
this.totalCountCache = giraffeCount + sealCount;
}
public int getGiraffeCount() {
return giraffeCount;
}
public int getSealCount() {
return sealCount;
}
public int getTotalCount() {
return totalCountCache;
}
}
We can force total count to be ignored using following mix-in:
public abstract class HasPropertiesToIgnoreZooMixIn {
@JsonIgnore
public abstract int getTotalCount();
}
Instead of conclusion
Hope this helps and leave comment below if you want me to cover specific topic, thanks!