Core Java

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.

Fig 1: Output from running example of Polymorphic Deserialization Using @JsonTypeInfo and @JsonSubTypes Annotations
Fig 1: Output from running example of Polymorphic Deserialization Using @JsonTypeInfo and @JsonSubTypes Annotations

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.

Download
You can download the full source code of this example here: Reflections vs. JsonSubTypes for Polymorphic Deserialization in Jackson

Omozegie Aziegbe

Omos holds a Master degree in Information Engineering with Network Management from the Robert Gordon University, Aberdeen. Omos is currently a freelance web/application developer who is currently focused on developing Java enterprise applications with the Jakarta EE framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button