Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michael-simons/04807990778a10a233e55bb7a2f6d6bc to your computer and use it in GitHub Desktop.
Save michael-simons/04807990778a10a233e55bb7a2f6d6bc to your computer and use it in GitHub Desktop.

Howto serialize and deserialize cyclomatic references in Java objects with Jacksons to and from JSON

1. Introduction

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].

1.1. The depending class

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;
     }
}

1.2. The owning class

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);
    }
}

2. Using Mixins

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.

3. The base application

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);
        }
    }
}
  1. Add some helpful modules

  2. Here we create a parent and add a child, thus forming a circle

  3. Serialize to JSON

  4. Assert everything is as expected

4. The failing example

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

5. The simple and often wrong solution: @JsonIgnore

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);
    }
}
  1. 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

6. Using managed references

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);
    }
}
  1. @JsonManagedReference goes into the parent class onto the attribute that holds the managed beans.

  2. Whereas the @JsonBackReference goes into the child class onto, well, the back reference of the owning object.

Output is as follows:

$ java ManagedReferenceApplication.java

7. Using @JsonIdentityInfo

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

8. Personal preferences

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment