Enterprise Java

Spring Custom Serializers with @JsonIdentityInfo

Intro

Serialization/Deserialization from/to JSON in Spring is widely used in modern Spring-based applications. It is based on Jackson. Jackson can serialize any POJO into JSON and vice versa with ease. This code is well written. I never encountered any issues. It gets more difficult when custom serializers are involved. This post shows how to use custom serializers in Spring with autowired fields.

Defining a Custom Serializer

Usually a custom serializer for a class is inherited from 
com.fasterxml.jackson.databind.ser.std.StdSerializer. This class defines some constructors but the framework only need a no-argument constructor that should call the superclass, something like this:

public CustomSerializer() {
    this(null);
}

public CustomSerializer(Class<ObjectToSerialize> t) {
    super(t);
}

Then there is the main method that must be implemented to actually write the JSON:

@Override
public void serialize(ObjectToSerialize value, JsonGenerator gen, SerializerProvider provider) throws IOException {
    gen.writeStartObject();
    ...
    provider.defaultSerializeField("some field", value.getField(), gen);
    ...
    gen.writeEndObject();
}

When the serializer class is created it must be registered as the serializer for ObjectToSerialize. This can be done with the @JsonSerialize annotation on the class:

@JsonSerialize(using = CustomSerializer.class)
public class ObjectToSerialize {

Now Jackson will be using this custom serializer for all instances of this class. If necessary a custom deserializer can be written by subclassing
com.fasterxml.jackson.databind.deser.std.StdDeserializer<T>

Circular References and @JsonIdentityInfo

For most commercial applications with Spring and Hibernate the issue of circular references manifests itself sooner or later. Here is a simple example. 

We have 2 classes:

public class Building {

    @Id
    @GeneratedValue(<parameters>)
    private Long id;

    private Set<Apartment> apartments;
}

public class Apartment {

    @Id
    @GeneratedValue(<parameters>)
    private Long id;

    private Building building;
}

If we try to serialize one building that has at least one apartment we get a StackOverflowException.

Jackson has a solution to this problem – @JsonIdentityInfo.

If the annotation @JsonIdentityInfo is added to the class like this:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class ObjectToSerialize {

then any ObjectMapper will break the cycle by replacing every occurrence of the object except the first with its id. Like this:

{
    "id": 1,
    "apartments": [
        {
            "id": 2,
            "building": 1 - the object is replaced with its ID
        },
        {
            "id": 3,
            "building": 1 - the object is replaced with its ID
        }
    ]
}

These are the tools Jackson provides to customize serialization and deal with circular references.

JSON Structure Problem

Problem

@JsonIdentityInfo works well for simple applications. But as the application grows in the default form it could affect the structure of the JSON. For example if some method returns the buildings and the districts in one response here is what may occur:

{
    "buildings": [
        {
            "id": 1,
            "apartments": [
                {
                    "id": 2,
                    "building": 1 - the object is replaced with its ID
                },
                {
                    "id": 3,
                    "building": 1 - the object is replaced with its ID
                }
            ]
        }
    ],
    "districts": [
         {
             "buildings": [
                 {
                     "id": 5,
                     ...
                 },
                 1, - the object is replaced with its ID
                 {
                     "id": 6,
                     ...
                 }
             ]
         }
    ]
}

This replacement could be quite unpredictable from the parser’s point of view. Within an array it could encounter objects and IDs. And this could happen for any field and any object. Any object where the class is annotated with @JsonIdentityInfo is replaced with its ID if the serialization provider finds it more than once. Every second, third, fourth etc. instance with the same ID found by the serialization provider is replaced with its ID. 

Solution

The solution here is to use a separate ObjectMapper to write parts of the JSON. The lists of already seen IDs are stored in the serialization provider which is created by ObjectMapper. By creating a separate ObjectMapper (with a probably different configuration) the lists are reset. 

For a “composite” JSON result which returns different objects types a custom serializer can be written. In this custom serializer the “header” is written manually with JsonGenerator methods and when the correct level in the JSON is reached we create a new ObjectMapper and write a much better looking JSON.

{
    "buildings": [ - create a new ObjectMapper
        {
            "id": 1,
            "apartments": [
                {
                    "id": 2,
                    "building": 1 - the object is replaced with its ID
                },
                {
                    "id": 3,
                    "building": 1 - the object is replaced with its ID
                }
            ]
        }
    ],
    "districts": [ - create a new ObjectMapper
         {
             "buildings": [
                 {
                     "id": 5,
                     ...
                 },
                 { - the object is written as a JSON Object not an ID
                     "id": 1,
                     ...
                 },
                 {
                     "id": 6,
                     ...
                 }
             ]
         }
    ]
}

To write the JSON to the original generator we can use
ObjectMapper.writeValueAsString and
JsonGenerator.writeRawValue(String). 

P.S. it is also possible to create a new serialization provider by means of
DefaultSerializerProvider.createInstance(SerializationConfig, SerializerFactory) but it is potentially more complicated. 

Custom Serializer Autowire Problem

Problem

We’d like to be able to use @Autowire in our custom serializers. It is one of Spring’s best features! Actually it works if the default ObjectMapper is used. But if we use the solution to the JSON structure problem it doesn’t work for custom serializers instantiated by our own object mappers. 

Solution

Our own object mappers must be configured with a special HandlerInstantiator:

// try to use the default configuration as much as possible
ObjectMapper om = Jackson2ObjectMapperBuilder.json().build();
// This instantiator will handle autowiring properties into the custom serializers
om.setHandlerInstantiator(
new SpringHandlerInstantiator(this.applicationContext.getAutowireCapableBeanFactory()));

If the custom object mappers are created inside another custom serializer which is created by the default ObjectMapper then it can autowire the ApplicationContext.

Published on Java Code Geeks with permission by Vadim Korkin, partner at our JCG program. See the original article here: Spring Custom Serializers with @JsonIdentityInfo

Opinions expressed by Java Code Geeks contributors are their own.

Vadim Korkin

Vadim is a senior software engineer with lots of experience with different software technologies including Java, Javascript, databases (Oracle and Postgres), HTML/CSS and even machine learning. He likes learning new technologies and using the latest libraries in his work. Vadim also has some personal projects on Github
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button