DEV Community

Bruno Oliveira
Bruno Oliveira

Posted on

Introduction to ObjectMappers: using Jackson in the wild

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:

  1. 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.
  2. 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)

Collapse
 
elcotu profile image
Daniel Coturel

Hi Bruno, good post.
It's interesting in order to try it.

Collapse
 
dharmanel profile image
dharmanel • Edited

Contains very good interesting information. Thank you for sharing. Will try the code

Collapse
 
kenduraghav profile image
Raghavendran S

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