1. Introduction
Serializing our complete data structure to JSON using an exact one-on-one representation of all the fields may not be appropriate sometimes or simply may not be what we want. Instead, we may want to create an extended or simplified view of our data. This is where custom Jackson serializers come into play.
However, implementing a custom serializer can be tedious, especially if our model objects have lots of fields, collections, or nested objects. Fortunately, the Jackson library has several provisions that can make this job a lot simpler.
In this short tutorial, we’ll take a look at custom Jackson serializers and show how to access default serializers inside a custom serializer.
2. Sample Data Model
Before we dive into the customization of Jackson, let’s have a look at our sample Folder class that we want to serialize:
public class Folder { private Long id; private String name; private String owner; private Date created; private Date modified; private Date lastAccess; private List<File> files = new ArrayList<>(); // standard getters and setters }
And the File class, which is defined as a List inside our Folder class:
public class File { private Long id; private String name; // standard getters and setters }
3. Custom Serializers in Jackson
The main advantage of using custom serializers is that we do not have to modify our class structure. Plus, we can easily decouple our expected behavior from the class itself.
So, let’s imagine that we want a reduced view of our Folder class:
{ "name": "Root Folder", "files": [ {"id": 1, "name": "File 1"}, {"id": 2, "name": "File 2"} ] }
As we’ll see over the next sections, there are several ways we can achieve our desired output in Jackson.
3.1. Brute Force Approach
First, without using Jackson’s default serializers, we can create a custom serializer in which we do all the heavy lifting ourselves.
Let’s create a custom serializer for our Folder class to achieve this:
public class FolderJsonSerializer extends StdSerializer<Folder> { public FolderJsonSerializer() { super(Folder.class); } @Override public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeStringField("name", value.getName()); gen.writeArrayFieldStart("files"); for (File file : value.getFiles()) { gen.writeStartObject(); gen.writeNumberField("id", file.getId()); gen.writeStringField("name", file.getName()); gen.writeEndObject(); } gen.writeEndArray(); gen.writeEndObject(); } }
Thus, we can serialize our Folder class to a reduced view containing only the fields that we want.
3.2. Using Internal ObjectMapper
Although custom serializers provide us the flexibility of altering every property in detail, we can make our job easier by reusing Jackson’s default serializers.
One way of using the default serializers is to access the internal ObjectMapper class:
@Override public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeStringField("name", value.getName()); ObjectMapper mapper = (ObjectMapper) gen.getCodec(); gen.writeFieldName("files"); String stringValue = mapper.writeValueAsString(value.getFiles()); gen.writeRawValue(stringValue); gen.writeEndObject(); }
So, Jackson simply handles the heavy lifting by serializing the List of File objects, and then our output will be the same.
3.3. Using SerializerProvider
Another way of calling the default serializers is to use the SerializerProvider. Therefore, we delegate the process to the default serializer of the type File.
Now, let’s simplify our code a little bit with the help of SerializerProvider:
@Override public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeStringField("name", value.getName()); provider.defaultSerializeField("files", value.getFiles(), gen); gen.writeEndObject(); }
And, just as before, we get the same output.
4. A Possible Recursion Problem
Depending on the use case, we may need to extend our serialized data by including more details for Folder. This might be for a legacy system or an external application to be integrated that we do not have a chance to modify.
Let’s change our serializer to create a details field for our serialized data to simply expose all the fields of the Folder class:
@Override public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeStringField("name", value.getName()); provider.defaultSerializeField("files", value.getFiles(), gen); // this line causes exception provider.defaultSerializeField("details", value, gen); gen.writeEndObject(); }
This time we get a StackOverflowError exception.
When we define a custom serializer, Jackson internally overrides the original BeanSerializer instance that is created for the type Folder. Consequently, our SerializerProvider finds the customized serializer every time, instead of the default one, and this causes an infinite loop.
So, how do we solve this problem? We’ll see one usable solution for this scenario in the next section.
5. Using BeanSerializerModifier
A possible workaround is using BeanSerializerModifier to store the default serializer for the type Folder before Jackson internally overrides it.
Let’s modify our serializer and add an extra field — defaultSerializer:
private final JsonSerializer<Object> defaultSerializer; public FolderJsonSerializer(JsonSerializer<Object> defaultSerializer) { super(Folder.class); this.defaultSerializer = defaultSerializer; }
Next, we’ll create an implementation of BeanSerializerModifier to pass the default serializer:
public class FolderBeanSerializerModifier extends BeanSerializerModifier { @Override public JsonSerializer<?> modifySerializer( SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) { if (beanDesc.getBeanClass().equals(Folder.class)) { return new FolderJsonSerializer((JsonSerializer<Object>) serializer); } return serializer; } }
Now, we need to register our BeanSerializerModifier as a module to make it work:
ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.setSerializerModifier(new FolderBeanSerializerModifier()); mapper.registerModule(module);
Then, we use the defaultSerializer for the details field:
@Override public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeStringField("name", value.getName()); provider.defaultSerializeField("files", value.getFiles(), gen); gen.writeFieldName("details"); defaultSerializer.serialize(value, gen, provider); gen.writeEndObject(); }
Lastly, we may want to remove the files field from the details since we already write it into the serialized data separately.
So, we simply ignore the files field in our Folder class:
@JsonIgnore private List<File> files = new ArrayList<>();
Finally, the problem is solved and we get our expected output as well:
{ "name": "Root Folder", "files": [ {"id": 1, "name": "File 1"}, {"id": 2, "name": "File 2"} ], "details": { "id":1, "name": "Root Folder", "owner": "root", "created": 1565203657164, "modified": 1565203657164, "lastAccess": 1565203657164 } }
6. Conclusion
In this tutorial, we learned how to call default serializers inside a custom serializer in Jackson Library.
Like always, all the code examples used in this tutorial are available over on GitHub.