Codementor Events

Spring Boot - Abstract @RequestBody

Published Jun 10, 2022Last updated Dec 06, 2022
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

Find a lot more posts like this one on my technical blog where I share ideas and implementations of real-world production issues that I faced.😁 https://new-spike.net

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

Find a lot more posts like this one on my technical blog where I share ideas and implementations of real-world production issues that I faced.😁 https://new-spike.net

Discover and read more posts from Andrei Saizu
get started
post commentsBe the first to share your opinion
ahmed royce
2 years ago

THANKS FOR SHARING
https://testmyspeed.onl/

Andrei Saizu
2 years ago

Hope it’s helpful! :) You can check out more posts like this on my technical blog new-spike.net !

Show more replies