This Refactorizando entry on Serialization and Deserialization in Inheritance with Jackson is a continuation of the previous article on Serialization and Deserialization with Jackson.
As we mentioned in the previous article, Jackson is a library that helps us serialize and deserialize objects to map them to our class. In this article, we will explore what happens when we have inheritance.
We will cover two types of inheritance: when we want to exclude information and when we want to include it.
In this article, we will use Lombok in some of our examples.
Let’s start with the Maven dependencies for Jackson:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.1</version> </dependency>
Inclusion of Information in Serialization and Deserialization
Using ObjectMapper globally
For our examples, we will use a class called “Animal,” which will have two subclasses, “Dog” and “Ostrich.” We will also create a class “Vet” that will contain a list of animals.
Let’s start by defining our parent class:
@Getter @Setter public abstract class Animal { private String type; private int legsNumber; protected Animal(String type, int legsNumber) { this.type = type; this.legsNumber = legsNumber; } }
Next, we’ll create the “Dog” class:
@Getter @Setter public class Dog extends Animal { private String model; public Dog(String type, int legsNumber, String model) { super(type, legsNumber); this.model = model; } }
Now, let’s create the “Ostrich” subclass:
@Getter @Setter public class Ostrich extends Animal { private int age; public Ostrich(String type, int legsNumber, int age) { super(type, legsNumber); this.age = age; } }
Finally, we’ll create the “Vet” class, which will contain a list of animals:
@Getter @Setter public class Vet { private List<Animal> animals; }
To declare the information only once and globally, we will perform an activation in an ObjectMapper, which is the best option when dealing with multiple involved types. This activation will be done using the activateDefaultTyping
method.
The activateDefaultTyping
method accepts different parameters, and it is mandatory to use PolymorphicTypeValidator
as a security parameter for all cases.
Let’s proceed to globally activate the mapper and also instantiate two objects, one Dog, and one Ostrich, and add them to the Vet class. All of this will be done through a test class:
@Test public void shouldSerializeAnimals() { PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator .builder() .allowIfSubType(Animal.class) .build(); ObjectMapper mapper = JsonMapper.builder() .activateDefaultTyping(ptv, DefaultTyping.NON_CONCRETE_AND_ARRAYS) .build(); Dog dog = new Dog("mammal", 4, "Huski"); Ostrich ostrich = new Ostrich("oviparous ", 2, 5); List<Animal> animals = new ArrayList<>(); animals.add(dog); animals.add(ostrich); Vet vet = new Vet(); vet.setAnimals(animals); mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet); System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet)); }
Output: { "animals" : [ "java.util.ArrayList", [ [ "com.refactorizando.jackson.Dog", { "type" : "mammal", "legsNumber" : 4, "model" : "Huski" } ], [ "com.refactorizando.jackson.Ostrich", { "type" : "oviparous ", "legsNumber" : 2, "age" : 5 } ] ] ] }
Annotations on Classes
Although the above approach works and is easy to understand, it adds a lot of boilerplate code, which can sometimes make it more complicated than necessary.
Jackson provides annotations that simplify programming for serializing and deserializing classes. For inheritance, we will use the @JsonTypeInfo
annotation for the parent class and the @JsonSubTypes
annotation for the child classes. This information should be added to the parent class, as shown in the previous example:
@Getter @Setter @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes({ @Type(value = Dog.class, name = "dog"), @Type(value = Ostrich.class, name = "ostrich") }) public abstract class Animal { private String type; private int legsNumber; protected Animal(String type, int legsNumber) { this.type = type; this.legsNumber = legsNumber; } }
A notable point is that we have added the “type” property, mapped with the values “dog” and “ostrich.” This way, when our JSON is displayed, it will have a new field.
Let’s execute the following test:
@Test public void shouldSerializeAnimals() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); DogInclusionAnnotation dog = new DogInclusionAnnotation("mammal", 4, "Huski"); OstrichInclusionAnnotation ostrich = new OstrichInclusionAnnotation("oviparous ", 2, 5); List<AnimalInclusionAnnotation> animals = new ArrayList<>(); animals.add(dog); animals.add(ostrich); VetInclusionAnnotation vet = new VetInclusionAnnotation(); vet.setAnimalInclusionAnnotations(animals); System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet)); } }
Output
Output: { "animalInclusionAnnotations" : [ { "animalType" : "dog", "type" : "mammal", "legsNumber" : 4, "model" : "Huski" }, { "animalType" : "ostrich", "type" : "oviparous ", "legsNumber" : 2, "age" : 5 } ] }
Ignoring Properties in Inheritance
In many cases, we need to ignore certain fields in our classes so that they are not mapped by the incoming or outgoing values. For such cases, let’s explore different ways to ignore them.
Ignoring Properties with Annotations
In general, using annotations is the most convenient and practical way, and it helps reduce the amount of code.
Let’s use the previous example’s parent class “Animal” and its child classes:
@JsonIgnoreProperties({ "type", "model" }) @Getter @Setter public class DogIgnoreAnnotation extends AnimalIgnoreAnnotation{ private String model; private String name; public DogIgnoreAnnotation(String type, int legsNumber, String model, String name) { super(type, legsNumber); this.model = model; this.name = name; } }
With the above annotations, we ignore both the parent and child properties using JsonIgnoreProperties
. Additionally, if we want to ignore a specific property, we can use @JsonIgnore
.
Let’s see how to execute it:
@Test public void shouldSerializeAnimals() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); DogIgnoreAnnotation dog = new DogIgnoreAnnotation("mammal", 4, "Huski", "bruno"); OstrichIgnoreAnnotation ostrich = new OstrichIgnoreAnnotation("oviparous ", 2, 5); List<AnimalIgnoreAnnotation> animals = new ArrayList<>(); animals.add(dog); animals.add(ostrich); VetIgnoreAnnotation vet = new VetIgnoreAnnotation(); vet.setAnimalIgnoreAnnotationList(animals); mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet); System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet)); }
Output: { "animalIgnoreAnnotationList" : [ { "legsNumber" : 4, "name" : "bruno" }, { "type" : "oviparous ", "legsNumber" : 2, "age" : 5 } ] }
Ignoring Properties Using Jackson Introspection
Ignoring properties in your class or classes through Jackson introspection is not very common or well-known, but it is a powerful way to ignore properties. This method allows us to use hasIgnoreMarker
to mark the properties to be avoided.
To use Jackson introspection, we need to extend the JacksonAnnotationIntrospector
class.
For the following example, let’s ignore the “type” and “model” of a dog as we did previously:
class JacksonIntrospectorExample extends JacksonAnnotationIntrospector { public boolean hasIgnoreMarker(AnnotatedMember annotatedMember) { return mannotatedMember.getDeclaringClass() == Animal.class && annotatedMember.getName() == "type" || annotatedMember.getDeclaringClass() == Dog.class || annotatedMember.getName() == "legsNumber" || super.hasIgnoreMarker(annotatedMember); } }
@Test public void shouldSerializeAnimals() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); mapper.setAnnotationIntrospector(new JacksonIntrospector()); DogIgnoreIntrospection dog = new DogIgnoreIntrospection("mammal", 4, "Huski"); OstrichIgnoreIntrospection ostrich = new OstrichIgnoreIntrospection("oviparous ", 2, 5); List<AnimalIgnoreIntrospection> animals = new ArrayList<>(); animals.add(dog); animals.add(ostrich); VetIgnoreIntrospection vet = new VetIgnoreIntrospection(); vet.setAnimalIgnoreIntrospections(animals); mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet); System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet)); }
Output: { "animalIgnoreIntrospections" : [ { "animalType" : "dog" }, { "animalType" : "ostrich", "age" : 5 } ] }
Conclusion
In this article about Serialization and Deserialization in Inheritance with Jackson, we have explored various ways to add and ignore fields in our classes, using annotations and Java classes in an easy and straightforward manner. Jackson is undoubtedly a highly useful library in our applications.
If you wish, you can view the example code on our GitHub repository.