Introduction
This post is motivated by some recent real-world work I had to do involving the Jackson ObjectMapper and in it I'll try to give an explanation as to why it matters as well as some of its cool tricks and quirks. I'll also enumerate some of its real-world use scenarios.
What is Jackson ObjectMapper
Quoting the documentation:
ObjectMapper provides functionality for reading and writing JSON, either to and from basic POJOs (Plain Old Java Objects), or to and from a general-purpose JSON Tree Model (JsonNode), as well as related functionality for performing conversions. It is also highly customizable to work both with different styles of JSON content, and to support more advanced Object concepts such as polymorphism and Object identity. ObjectMapper also acts as a factory for more advanced ObjectReader and ObjectWriter classes. Mapper (and ObjectReaders, ObjectWriters it constructs) will use instances of JsonParser and JsonGenerator for implementing actual reading/writing of JSON. Note that although most read and write methods are exposed through this class, some of the functionality is only exposed via ObjectReader and ObjectWriter: specifically, reading/writing of longer sequences of values is only available through ObjectReader.readValues(InputStream) and ObjectWriter.writeValues(OutputStream).
That's a lot to process, but, the key takeaway is right at the beginning:
ObjectMapper provides functionality for reading and writing JSON, either to and from basic POJOs (Plain Old Java Objects), or to and from a general-purpose JSON Tree Model (JsonNode), as well as related functionality for performing conversions.
This is very powerful, for several reasons:
- REST APIs usually return JSON responses from their endpoints, and there are multiple libraries to extract information from JSON
- JSON is a standard format that's agreed upon, universal and can be passed around as Strings, read by standard text editors, etc. In other words: it's simple, versatile and very very useful to work with and understand
The power of abstracting away from the Java representation of these objects, is that it encapsulates them and doesn't expose them when exchanging information about them directly, which is good.
As an example, a web front-end needs only to concern with a textual representation of the domain model that it's consuming in order to display it to the end users. JSON is the perfect format for the job.
An example domain model
Let's see how the ObjectMapper works with simple examples from a nice domain: a restaurant, a food truck, chefs and good food :)
Our restaurant will have daily food menus, chefs preparing those menus and customers visiting the restaurant. Each of these entities can be represented by the following Java classes:
public class FoodtruckOwner implements Chef {
private final String name;
private MainDish mainDish;
private String establishment;
public FoodtruckOwner(String name, MainDish mainDish, String establishmentType){
this.name = name;
this.mainDish = mainDish;
this.establishment=establishmentType;
}
public void setChefsMainDish(MainDish mainDish) {
this.mainDish=mainDish;
}
public MainDish getChefsMainDish() {
return mainDish;
}
public String getEstablishmentType() {
return establishment;
}
public void setEstablishmentType(String typeOfEstablishment) {
this.establishment=typeOfEstablishment;
}
}
public class RestaurantChef implements Chef{
private final String name;
private MainDish mainDish;
private String establishment;
private final int michelinStars;
public RestaurantChef(String name, MainDish mainDish, String establishmentType,int michelinStars){
this.name = name;
this.mainDish = mainDish;
this.establishment=establishmentType;
this.michelinStars=michelinStars;
}
public void setChefsMainDish(MainDish mainDish) {
this.mainDish=mainDish;
}
public MainDish getChefsMainDish() {
return mainDish;
}
public String getEstablishmentType() {
return establishment;
}
public void setEstablishmentType(String typeOfEstablishment) {
this.establishment=typeOfEstablishment;
}
}
Then we have the FoodEstablishment
class:
//Can be a restaurant or a food truck
public class FoodEstablishment {
private final List<Chef> chefs;
private final int lotation;
private final String type;
public FoodEstablishment(List<Chef> chefs, int lotation, String type){
this.chefs = chefs;
this.lotation = lotation;
this.type = type;
}
public int getLotation() {
return lotation;
}
public List<Chef> getChefs() {
return chefs;
}
public String getType() {
return type;
}
}
And the Chef
interface:
public interface Chef {
void setChefsMainDish(MainDish mainDish);
MainDish getChefsMainDish();
String getEstablishmentType();
void setEstablishmentType(String typeOfEstablishment);
}
Let's go over this small domain to start with.
We have a Cook
interface that gets implemented by both the RestaurantChef
and FoodtruckOwner
since both of them will have a main dish they specialize at and both of them know exactly the type of establishment they own ( duh :D ).
We also have a more general class representing a FoodEstablishment
which can be of course, one of either, a restaurant or a food truck.
For the purpose of understanding the Jackson ObjectMapper and its inner workings, note that we have the chefs
attribute in the FoodEstablishment
class which is of type "list of Chef
interface", so, a collection parametrized by an interface. This will be important later on, when we will focus on deserialization.
Diving in: Serialization and Deserialization using Jackson
Now that we introduced our small domain for learning about Jackson, let's dive into some details.
Serialization
Serialization is the process of writing a Java Object into JSON format (on the web you find references to POJOs (Plain Old Java Objects) but, basically, you only need a class that you want to serialize in your domain to follow some properties and anything can be serialized.
Let's look at exercising a small unit test to serialize to a JSON file the FoodtruckOwner
class:
class FoodtruckOwnerMapperTest {
ObjectMapper mapper = new ObjectMapper();
@Test
public void serialization() throws IOException {
FoodtruckOwner owner = new FoodtruckOwner("John", new MainDish("rice",6.7),"foodTruck");
File jsonOwner = new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json");
mapper.writeValue(jsonOwner, owner);
}
}
To see how serialization works, first an instance of the class we want to serialize is created, and a file to store the serialized version is created as well.
Then, we invoke the writeValue
method of the mapper class, which can throw an exception, that we set to be thrown in the test method itself, so we can see when things are failing and why. The mapper can throw an exception because the serialization might fail at times.
If we run the test, it will pass, and if we inspect the file contents of the owner.json
file, here's what we have:
{"chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck"}
You know now how to serialize a class from Java into JSON!
However, some important points:
- The name of the cook is "John" but it is not in the JSON file.
- The "keys" in the JSON file are set from the names of the getter methods in the class.
Annotations and ObjectMapper requirements
The name of the cook is not in the serialized version, because we do not have a getter for that property. So, in order to be able to serialize attributes, we need to have getters for the properties we want to serialize. If we add the missing getter to the class, even with a more specific name:
public String getFoodtruckOwnerName() {
return name;
}
Then our serialized version is:
{"chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck","foodtruckOwnerName":"John"}
So, regarding serialization we learned:
- We need getters for the properties that we want to be serialized.
- The actual method name of the getter is used in the JSON as a key to identify the attribute.
Using annotations to configure the serialization
We can use annotations that allow us to configure the serialization. For instance, "foodtruckOwnerName" seems quite verbose, maybe we want to just serialize the attribute with the key "name".
For that, we can use the annotation @JsonProperty
on the getter method for the attribute that receives as argument the new name for the serialization, so, like this:
@JsonProperty("nameOfCook")
public String getFoodtruckOwnerName() {
return name;
}
we get the following JSON:
{"chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck","nameOfCook":"John"}
The last property we can be interested in look at, is for instance, to not serialize fields with null values, say, we receive a class instance where someone forgot to set the name of the cook, so it's null. We get then this JSON:
{"chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck","nameOfCook":null}
If we don't want to have the field with null value included in our JSON, we can do two things:
- Use the
@JsonInclude(JsonInclude.Include.NON_NULL)
annotation at the class level, which will ignore all the fields with a null value in the class. - Use the same annotation at field level, which will ignore only specific null valued fields.
And for this tutorial, this will be it regarding serialization. Onto deserialization.
Deserialization
Deserialization with Jackson
Now that we've covered the basics of serialization, it's time to move onto the reverse process, the deserialization.
When deserializing, we receive as input a JSON file, and we map it into a Java Object of the corresponding class we want to deserialize it into. Using the mapper, this is how it looks:
@Test
void deserialization() throws IOException {
FoodtruckOwner owner = new FoodtruckOwner("John", new MainDish("rice",6.7),"foodTruck");
File jsonOwner = new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json");
mapper.writeValue(jsonOwner, owner);
FoodtruckOwner chef = mapper.readValue(new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json"),
FoodtruckOwner.class);
}
So, we do the serialization as above, and we try to deserialize it from JSON back into our domain model class, the FoodtruckOwner
. However, we get this error:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `objectmapper.domain.FoodtruckOwner` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (File); line: 1, column: 2]
This is somewhat unexpected. After correctly managing to serialize the class into JSON, we run into unforeseen troubles when reading it back into Java.
Jackson requires that there's an empty default constructor on the target class, so, if we have this now:
FoodtruckOwner(){
}
public FoodtruckOwner(String name, MainDish mainDish, String establishmentType){
this.name = name;
this.mainDish = mainDish;
this.establishment=establishmentType;
}
it will surely work, right?
Not quite, it turns out that we had the same missing default constructor in our MainDish
class... This is starting to get complex. Let's add the default constructor to our MainDish
class.
After adding the default constructor to the MainDish
class, we get what we want, the test is green and we have our instance in Java, correctly deserialized:
@Test
void deserialization() throws IOException {
FoodtruckOwner owner = new FoodtruckOwner("John", new MainDish("rice",6.7),"foodTruck");
File jsonOwner = new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json");
mapper.writeValue(jsonOwner, owner);
FoodtruckOwner chef = mapper.readValue(new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json"),
FoodtruckOwner.class);
assertEquals("John", chef.getName());
final MainDish chefsMainDish = chef.getChefsMainDish();
assertNotNull(chefsMainDish);
assertEquals("rice", chefsMainDish.getName());
assertEquals(6.7, chefsMainDish.getCalories());
}
So, we see that, after adding default constructors, our deserialization process worked and we managed to correctly read in our JSON representing the food truck owner back into Java. However there were some issues.
It was necessary to modify the Java classes in quite intrusive ways, since we needed to explicitly add default constructors to these classes that are mere "clients" from the perspective of the deserialization process. This is not good.
What if the MainDish
class was actually just a library to which we didn't have direct access to? Then these modifications in the source code would be impractical if not at all impossible (not impossible, tools like "asm inspector" in IntelliJ can effectively decompile libraries into source code that you can copy and adapt to your needs, but, this is not the way).
Let's assume that such scenario is what we are facing: we can't modify the source of MainDish
. How to approach it then?
Jackson MixIns
When we can't modify the source code directly to adhere to the conventions imposed by Jackson, we need to resort to some special functionalities that allow Jackson to interact "indirectly" with 3rd party code.
One such functionality is called "Mix-Ins".
"Mix-in" annotations are a way to associate annotations with classes, without modifying (target) classes themselves, originally intended to help support 3rd party datatypes where user can not modify sources to add annotations.
With mix-ins you can:
- Define that annotations of a '''mix-in class''' (or interface) will be used with a '''target class''' (or interface) such that it appears as if the ''target class'' had all annotations that the ''mix-in'' class has (for purposes of configuring serialization / deserialization) You can think of it as kind of aspect-oriented way of adding more annotations during runtime, to augment statically defined ones.
This is exactly what is needed for our case, since we are unable to modify the MainDish
class.
We can use the following mix-in:
public abstract class MainDishMixin extends MainDish {
@JsonCreator
public MainDishMixin(@JsonProperty("name") String name, @JsonProperty("calories") double calories) {
super(name, calories);
}
}
Here we note some important things:
The mix-in must be defined in a different package than the target class, and it can not be defined as an inner class of the target
The parameters that emulate the original constructor of the class are annotated with the
@JsonProperty
annotation and the same names and types as in the target class
Then we register the MixIn in our ObjectMapper class using mapper.addMixIn(MainDish.class, MainDishMixin.class);
and we are done!
Then the test will pass and we achieved what we wanted: without directly modifying the target class, we managed to successfully deserialize it into a Java object.
Deserializing interface fields in classes
Our last feature will be how to deserialize fields that are parametrized as interfaces in our code. Let's assume that we added all getters and setters to the classes RestaurantChef
and FoodtruckOwner
, serialized it and now have this JSON in our hands:
{"chefs":[{"name":"Michel","michelinStars":4,"chefsMainDish":{"name":"Chicken","calories":20.0},"establishmentType":"Restaurant"},{"name":"John","chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck"}],"lotation":10}
Let's assume for this last example we have this class:
//Can be a restaurant or a food truck
public class FoodEstablishment {
private List<Chef> chefs;
private int lotation;
public FoodEstablishment(){}
public FoodEstablishment(List<Chef> chefs, int lotation){
this.chefs = chefs;
this.lotation = lotation;
}
public int getLotation() {
return lotation;
}
public List<Chef> getChefs() {
return chefs;
}
}
Unfortunately, attempting to deserialize this class as is, will bring us to yet one more error:
DefinitionException: Cannot construct instance of `objectmapper.domain.Chef` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (File); line: 1, column: 11] (through reference chain: objectmapper.domain.FoodEstablishment["chefs"]->java.util.ArrayList[0])
What this means is that since Chef
is being used as an interface there, Jackson won't know how to deserialize it, because the interface is actually shared by two implementations. To make this work we need to annotate the interface with additional information regarding the different sub-types:
@JsonTypeInfo(
use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.PROPERTY,
property = "class")
@JsonSubTypes({
@JsonSubTypes.Type(value = RestaurantChef.class, name = "restaurantChef"),
@JsonSubTypes.Type(value = FoodtruckOwner.class, name = "foodtruckOwner")})
public interface Chef {
void setChefsMainDish(MainDish mainDish);
MainDish getChefsMainDish();
String getEstablishmentType();
void setEstablishmentType(String typeOfEstablishment);
}
After adding the above annotation, the information provided by it, allows Jackson to deduce when deserializing which implementation to use, and everything will work!
Conclusions
I hope this post provided a good overall introduction to the process of serialization and deserialization in Java while using Jackson and that it'll allow to explore even more from here on. Thanks for reading!
Top comments (3)
Hi Bruno, good post.
It's interesting in order to try it.
Contains very good interesting information. Thank you for sharing. Will try the code
Found very useful. Recently I faced an issue like I dont have access for a pojo class. Now I can fix it using with Mix-ins