Reflections vs. JsonSubTypes for Polymorphic Deserialization in Jackson
Polymorphic deserialization is a concept in the world of Java and JSON processing, and Jackson, a JSON parsing library provides support for it. In this article, we’ll explore the fundamentals of polymorphic deserialization in Jackson, demonstrating how it empowers developers to work with JSON data utilizing annotations like @JsonTypeInfo
and @JsonSubTypes
. We will also explore how to use @JsonTypeInfo
with Jackson and Reflections for polymorphic deserialization.
1. Introduction
Polymorphic deserialization in Jackson is a technique that allows developers to deserialize JSON data into Java objects when the exact type of the object is not known in advance. This is particularly useful in scenarios where we have a class hierarchy with multiple subclasses or when dealing with dynamic data structures.
Jackson provides a feature called @JsonTypeInfo
that allows developers to achieve polymorphic deserialization. Jackson’s polymorphic deserialization capabilities are essential when working with JSON data that can represent different types of objects based on contextual or runtime information. It empowers developers to handle complex JSON structures gracefully, mapping them to the appropriate Java classes or interfaces seamlessly.
To enable polymorphic deserialization in Jackson, you utilize annotations like @JsonTypeInfo
and @JsonSubTypes
. These annotations provide a means to specify how type information should be included in the JSON data and how it should be used to determine the correct Java class during deserialization.
In this process, Jackson identifies and utilizes type hints in the JSON data to create instances of the appropriate Java classes or interfaces, ensuring that the deserialization process respects the structure of the data and the class hierarchy.
2. Polymorphic Deserialization Using @JsonTypeInfo and @JsonSubTypes Annotations
In Jackson, Java developers can achieve polymorphic deserialization using @JsonTypeInfo
annotations and @JsonSubTypes
.
2.1 @JsonTypeInfo
@JsonTypeInfo
is an annotation used to include type information in the JSON. It allows you to specify how the type information should be represented in the JSON, such as using a property, wrapper object, or none at all.
2.2 @JsonSubTypes
@JsonSubTypes
is used to specify subtypes of a class/interface, allowing Jackson to determine the actual class during deserialization based on the type information provided in the JSON.
2.3 Example 1: Using @JsonTypeInfo and @JsonSubTypes Annotations
Using Jackson Annotations, We can use Jackson annotations like @JsonTypeInfo
and @JsonSubTypes
to define type information and specify subtypes. Below is an example to illustrate this concept:
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type" ) @JsonSubTypes({ @JsonSubTypes.Type(value = Dog.class, name = "dog"), @JsonSubTypes.Type(value = Cat.class, name = "cat") }) public class Animal { public Animal() { } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @JsonTypeName("dog") public static class Dog extends Animal { public Dog() { } // Dog-specific fields and methods private String breed; public String getBreed() { return breed; } public void setBreed(String breed) { this.breed = breed; } } @JsonTypeName("cat") public static class Cat extends Animal { // Cat-specific fields and methods private int numberOfLives; public Cat() { } public int getNumberOfLives() { return numberOfLives; } public void setNumberOfLives(int numberOfLives) { this.numberOfLives = numberOfLives; } } public static void main(String[] args) throws IOException { //Create an instance of the ObjectMapper class and configure it to handle polymorphic deserialization. ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enableDefaultTypingAsProperty(ObjectMapper.DefaultTyping.NON_FINAL, "type"); //Deserialize your JSON data into the base class String json = "{\"type\":\"dog\",\"name\":\"Fido\",\"breed\":\"Labrador\"}"; Animal animal = objectMapper.readValue(json, Animal.class); String jsonOutput = objectMapper.writeValueAsString(animal); System.out.println("" + jsonOutput); } }
In the example above, we start by defining the hierarchy of classes that we want to deserialize. We annotate the base class Animal
with @JsonTypeInfo
to indicate how type information should be included in the JSON. Also, we annotate the subclasses of Cat
and Dog
with @JsonTypeName
.
We created an instance of the ObjectMapper
class from the Jackson library and configured it to handle polymorphic deserialization. enableDefaultTypingAsProperty
is used to configure the ObjectMapper
to include type information in the JSON under the type
property.
Finally, We deserialize our JSON data into our base class. Jackson will automatically determine the correct subtype based on the type
property in the JSON. In this example, Jackson will deserialize the JSON data into an instance of the subclass Dog
. When we run the above code we should get the output as shown in Fig 1.
3. Polymorphic Deserialization Using Reflections
3.1 Reflections Overview
Reflections is a Java library that provides a convenient way to perform runtime reflection on classes, methods, fields, and annotations on Java applications. It allows developers to inspect and interact with the types and members of their program dynamically, without knowing their names or locations at compile-time.
Reflections are particularly useful for the following tasks:
- Discovering classes: Java programmers can use Reflections to scan packages and classpaths to discover classes that implement specific interfaces, extend certain base classes, or have particular annotations.
- Retrieving class metadata: Java programmers can extract information about classes, such as their fields, methods, constructors, and annotations, at runtime.
- Dynamic instantiation: Reflections allow Java programmers to create instances of classes dynamically, even if they don’t know the class names until runtime.
- Invoking methods: Java developers can invoke methods on objects dynamically, which can be helpful when dealing with different implementations of an interface.
- Accessing and modifying fields: Reflections enables Java programmers to access and modify the fields of an object, including private fields, which are not typically accessible directly.
3.2 Using Reflections with @JsonTypeInfo
Combining @JsonTypeInfo
with Reflections, Java developers can achieve polymorphic deserialization in a flexible way. Programmers can use Reflections with Jackson to achieve polymorphic deserialization when they need to discover and deserialize classes dynamically.
Here is a basic example of how to use the Reflections library for scanning and registering all existing subtypes:
First, we need to add the Reflections
library to our maven-based project:
<dependency> <groupId>org.reflections</groupId> <artifactId>reflections</artifactId> <version>0.10.2</version> </dependency>
Next, create the Java classes to represent the different types we want to deserialize. Make sure that they inherit from a common base class. Below are the Shape base class, and, Rectangle and Circle subclasses used in this example:
// Base class or interface public class Shape { public Shape() { } // Common properties and methods public static class Circle extends Shape { public Circle() { } private double radius; // Circle-specific properties and methods public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } } public static class Rectangle extends Shape { public Rectangle() { } private double width; private double height; // Rectangle-specific properties and methods public double getWidth() { return width; } public void setWidth(double width) { this.width = width; } public double getHeight() { return height; } public void setHeight(double height) { this.height = height; } } public static void main(String[] args) throws IOException { } }
Next, we annotate the Classes with @JsonTypeInfo
. We use the @JsonTypeInfo
annotation to indicate how the type information should be included in the JSON as shown below:
// Base class or interface @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") public class Shape { }
Next, Let’s create a custom annotation to Indicate type names of subtypes. To indicate the type name of each Shape subtype, we create a custom annotation named Shape_Type.java, with runtime visibility and applicable to types. In Netbeans, custom annotation can be added by right-clicking on the project name > Java > Java Annotation Type.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Shape_Type { String value(); }
3.3 Deserialize JSON with Jackson and Reflections
Next, we can use Jackson along with Reflections to deserialize some JSON data that includes type information and discover the appropriate class to instantiate as shown below:
public class Shape { public static void main(String[] args) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); String json = "{\"type\":\"circle\",\"radius\":5.0}"; // Deserialize the JSON into a Map to extract the type information Map jsonMap = objectMapper.readValue(json, new TypeReference<Map>() { }); String type = (String) jsonMap.get("type"); // Use Reflections to find the corresponding Java class Reflections reflections = new Reflections("com.javacodegeeks.shape"); Set<Class> subTypes = reflections.getSubTypesOf(Shape.class); Class shapeClass = subTypes.stream() .filter(clazz -> clazz.getSimpleName().equalsIgnoreCase(type)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("Unknown type: " + type)); // Deserialize the JSON into the determined class Shape shape = objectMapper.convertValue(jsonMap, shapeClass); String jsonOutput = objectMapper.writeValueAsString(shape); System.out.println("" + jsonOutput); } }
In the code above, we first deserialize the JSON data into a Map
to extract the type information. Next, we use Reflections to discover all subclasses of the Shape
class and then update the @JsonSubTypes
annotation with this information. Finally, we use Jackson’s convertValue
method to deserialize the JSON data into the determined class.
4. Conclusion
In summary, Reflections, and @JsonSubTypes in Jackson provides powerful means to achieve polymorphic deserialization in Java applications, making it easier to work with complex object hierarchies.
In conclusion, the combination of Reflections
and JsonSubTypes
in Jackson provides flexible solutions for achieving polymorphic deserialization in Java applications. Reflections enable dynamic classpath scanning, allowing Jackson to discover and register subtypes of a base class without the need for explicit configuration while JsonSubTypes, provides a declarative way to specify subtype information through annotations.
5. Download the Source Code
This was an example of Reflections vs. JsonSubTypes for Polymorphic Deserialization in Jackson.
You can download the full source code of this example here: Reflections vs. JsonSubTypes for Polymorphic Deserialization in Jackson