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
THANKS FOR SHARING
https://testmyspeed.onl/
Hope it’s helpful! :) You can check out more posts like this on my technical blog new-spike.net !