Spring Boot - Abstract @RequestBody

You must be shapeless, formless, like water. - Bruce Lee

Brief

Adding @RequestBody annotation on a @Controller endpoint, we automatically map the HttpRequest body to a Java object. But if we want to somehow get in the middle of the deserialization process, we'll take a look over some Jackson annotations.

Implementation

Annotation-based

Say we can receive an input like this ⬇️  where we have a field based on which we can differentiate between the types of objects. In our case, the type field.

{
    "type":"car",
    "gears": 5,
    "licenseNo":"BD51XWD"
}
Car
{
    "type":"airplane",
    "emergencyExits": 6,
    "cabinCrew": 8
}
Airplane

We create a Vehicle abstract class holding the common field. And then, two more classes Car and Airplane, both extending Vehicle, each with their own properties.

The trick here is to use the @JsonTypeInfo and @JsonSubTypes annotations in order to tell Jackson how to differentiate between different objects and to which types to cast the input.

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "type")   // field on which we differentiate objects
@JsonSubTypes({
        @JsonSubTypes.Type(value = Car.class, name = "car"),  // if value of 'type' field equals to 'car' instantiate a Car object
        @JsonSubTypes.Type(value = Airplane.class, name = "airplane")  // if value of 'type' field equals to 'airplane' instantiate an Airplane object
})
public abstract class Vehicle {
    private String type;

    public Vehicle(String type) {
        this.type = type;
    }
}
Vehicle.java
public class Car extends Vehicle {
    private Integer gears;
    private String licenseNo;

    public Car(Integer gears, String licenseNo, String type) {
        super(type);
        this.gears = gears;
        this.licenseNo = licenseNo;
    }
}
Car.java
public class Airplane extends Vehicle {
    private Integer emergencyExits;
    private Integer cabinCrew;

    public Airplane(Integer emergencyExits, Integer cabinCrew, String type) {
        super(type);
        this.emergencyExits = emergencyExits;
        this.cabinCrew = cabinCrew;
    }
}
Airplane.java

Now, on the controller, we can add the Vehicle abstract class on the request body and let Jackson do the magic.

@RestController
@RequestMapping("/test")
public class Controller {
    @PostMapping("/vehicle")
    public ResponseEntity<String> getVehicle(@RequestBody Vehicle vehicle) {
        if(vehicle instanceof Car){
            return ResponseEntity.ok("car");
        }
        else if (vehicle instanceof Airplane){
            return ResponseEntity.ok("airplane");
        }
        return ResponseEntity.badRequest().build();
    }
}
Controller.java
Car request
Airplane request

Custom deserializer

What do we do if we don't have a field which tells us what kind of object it is? For example, we get payloads like this ⬇️ for Dogs and Snakes.

{
    "legs": 4,
    "furColor": "black"
}
Dog
{
    "isLethal": true
}
Snake

We first, model the classes similar to the previous example. We need an Animal abstract class and two more classes Dog and Snake which extend Animal .

In addition, we need our own custom deserializer through which we tell Jackson how to instantiate the objects we want.

@JsonDeserialize(using = PayloadDeserializer.class)
public abstract class Animal {
}
Animal.java
public class Dog extends Animal {
    private Integer legs;
    private String furColor;

    public Dog(Integer legs, String furColor) {
        this.legs = legs;
        this.furColor = furColor;
    }
}
Dog.java
public class Snake extends Animal {
    private Boolean isLethal;

    public Snake(Boolean isLethal) {
        this.isLethal = isLethal;
    }
}
Snake.java
public class PayloadDeserializer extends StdDeserializer<Animal> {
    protected PayloadDeserializer() {
        this(null);
    }

    protected PayloadDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Animal deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        final JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        // in here you can add any logic you want
        if (node.has("legs")) {
            Integer legs = (Integer) (node.get("legs")).numberValue();
            String furColor = node.get("furColor").asText();

            return new Dog(legs, furColor);
        } else {
            Boolean isLethal = Boolean.valueOf(node.get("isLethal").textValue());
            return new Snake(isLethal);
        }
    }

}
PayloadDeserializer.java

And then, like before, we only add the Animal abstract class for the request's body type and that is it.

@RestController
@RequestMapping("/test")
public class Controller {
    @PostMapping("/animal")
    public ResponseEntity<String> getAnimal(@RequestBody Animal animal) {
        if(animal instanceof Dog){
            return ResponseEntity.ok("dog");
        }
        else if (animal instanceof Snake){
            return ResponseEntity.ok("snake");
        }
        return ResponseEntity.badRequest().build();
    }
}
Controller.java
Snake request
Dog request

You can find the code in the Github repository here:

GitHub - andreiszu/abstract-request-body
Contribute to andreiszu/abstract-request-body development by creating an account on GitHub.

💡
Don't miss out on more posts like this! Susbcribe to our free newsletter!
💡
Currently I am working on a Java Interview e-book designed to successfully get you through any Java technical interview you may take.
Stay tuned! 🚀