Howto serialize and deserialize cyclomatic references in Java objects with Jacksons to and from JSON
I work at Neo4j on our Java based object mappers, Neo4j-OGM and Spring Data Neo4j. In this role and many times before, when working with JPA / Hibernate, I realized that many people like to map one-to-many or one-to-one relationships in both directions: From parent to child and back. This may often look like in the following two classes, [Child.java] and [Parent.java].
public final class Child {
public final Integer id;
public final String name;
private Parent owner;
public Child(Integer id, String name) {
this.id = id;
this.name = name;
}
public Parent getOwner() {
return owner;
}
void setOwner(Parent owner) {
this.owner = owner;
}
}
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public final class Parent {
public final Integer id;
public final String name;
private final Set<Child> children = new HashSet<>();
public Parent(Integer id, String name) {
this.id = id;
this.name = name;
}
public Child addChild(Child child) {
if (child.getOwner() == null) {
this.children.add(child);
child.setOwner(this);
}
return child;
}
public Collection<Child> getChildren() {
return Collections.unmodifiableCollection(children);
}
}
I am a big fan of Jackson mixins. In object-oriented programming languages, a mixin is a class that contains methods for use by other classes without having to be the parent class of those other classes. Mixins are sometimes described as being "included" rather than "inherited". Through a mixin I can keep the above classes free of Jackson specific code. All the annotations go in the mixin.
The following class provides us with an instance of Jacksons ObjectMapper
configured with a couple of modules.
The parameter names module is especially handy, as it allows for immutable objects like Parent
and Child
presented above. It also takes care of executing our tests:
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
abstract class ApplicationBase {
private final ObjectMapper objectMapper;
ApplicationBase() {
this.objectMapper = new ObjectMapper();
this.objectMapper.registerModule(new Jdk8Module());
this.objectMapper.registerModule(new ParameterNamesModule()); // (1)
addMixins(this.objectMapper);
}
abstract void addMixins(ObjectMapper objectMapper);
final void run() throws Exception {
try {
var parent = new Parent(4711, "A parent"); // (2)
var child = parent.addChild(new Child(23, "A child"));
var jsonString = objectMapper.writeValueAsString(parent); // (3)
System.out.println("Serialized parent as " + jsonString);
System.out.println("Serialized child as " + objectMapper.writeValueAsString(child));
assertThat(objectMapper.readValue(jsonString, Parent.class) // (4)
.getChildren()).hasSize(1).first()
.satisfies(c -> assertThat(c.getOwner()).isNotNull());
} catch (Throwable e) {
System.out.println(e.getMessage());
System.exit(1);
}
}
}
-
Add some helpful modules
-
Here we create a parent and add a child, thus forming a circle
-
Serialize to JSON
-
Assert everything is as expected
The following application creates a parent / child relationship and tries to serialize it without further hints:
import com.fasterxml.jackson.databind.ObjectMapper;
class FailingApplication extends ApplicationBase {
public static void main(String...args) throws Exception {
new FailingApplication().run();
}
void addMixins(ObjectMapper objectMapper) {}
}
The application crashes:
$ java FailingApplication.java
How to fix this? One solution comes in quite often: @JsonIgnore
. While this works fine during serialization,
it will fail on deserialization:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
public class IgnorantApplication extends ApplicationBase {
public static void main(String...args) throws Exception {
new IgnorantApplication().run();
}
abstract static class JsonIgnoreMixin {
@JsonIgnore // (1)
abstract Parent getOwner();
}
void addMixins(ObjectMapper objectMapper) {
objectMapper.addMixIn(Child.class, JsonIgnoreMixin.class);
}
}
-
Ignore the parent property
Running the application is only a partial success. It serializes fine, but the assertion fails:
$ java IgnorantApplication.java
Why is this? @JsonIgnore
marks a property is completely ignore. It is not written during serialization and it
is not read during deserialization.
We add the property with a mixin and run the ignorant application
This problem can be solved by the combination of @JsonManagedReference
and @JsonBackReference
.
@JsonManagedReference
indicates a bean or a collection of beans managed by another bean.
In our case it looks like this:
import java.util.Collection;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ManagedReferenceApplication extends ApplicationBase {
public static void main(String...args) throws Exception {
new ManagedReferenceApplication().run();
}
abstract static class ParentMixin {
@JsonManagedReference // (1)
abstract Collection<Child> getChildren();
}
abstract static class ChildMixin {
@JsonBackReference // (2)
abstract Parent getOwner();
}
void addMixins(ObjectMapper objectMapper) {
objectMapper.addMixIn(Parent.class, ParentMixin.class);
objectMapper.addMixIn(Child.class, ChildMixin.class);
}
}
-
@JsonManagedReference
goes into the parent class onto the attribute that holds the managed beans. -
Whereas the @JsonBackReference goes into the child class onto, well, the back reference of the owning object.
Output is as follows:
$ java ManagedReferenceApplication.java
The third option changes the output. It basically tells Jackson how a bean can be identified. When working with a database, we often have such an identifier: The generated database id (or if we are really lucky, a unique business identifier).
What does Jackson do with that identifier? Jackson serializes the object normally when it sees it for the first time. If it comes across it a second time, it doesn’t do the whole dance a second (and a tripple ( and so on)) time, but writes down the id only.
It looks like this:
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.databind.ObjectMapper;
public class IdentifyInfoApplication extends ApplicationBase {
public static void main(String...args) throws Exception {
new IdentifyInfoApplication().run();
}
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
abstract static class IdentityInfoMixin {
}
void addMixins(ObjectMapper objectMapper) {
objectMapper.addMixIn(Parent.class, IdentityInfoMixin.class);
objectMapper.addMixIn(Child.class, IdentityInfoMixin.class);
}
}
Output is as follows:
$ java IdentifyInfoApplication.java
It’s of course up to the use case, which of the solutions is chosen. In a scenario with Spring Data REST, I would go with the 2nd one. It fits nicely with the resources exposed by REST. The first solution can be a fit as well.
If I would offer only a HTTP JSON Api and not deal with resources, I would maybe pick the 3rd one.
However, I would definitely ask the stake holders multiple times if cyclomatic back references are actually necessary in the domain itself. As you see with the addChild method, the are hard to maintain and I would avoid going that way.