Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I convert request body in a @RestController to a List of abstract values?

Let's say we have the following classes:

public abstract class Investment {

   private String investmentType;

   // getters & setters
}

public class Equity extends Investment {
}

public class Bond extends Investment {
}

public class InvestmentFactory {

    public static Investment getTypeFromString(String investmentType) {
        Investment investment = null;
        if ("Bond".equals(investmentType)) {
            investment = new Bond();
        } else if ("Equity".equals(investmentType)) {
            investment = new Equity();
        } else {
            // throw exception
        }
        return investment;
    }
}

And the following @RestController:

@RestController
public class InvestmentsRestController {

    private InvestmentRepository investmentRepository;

    @Autowired
    public InvestmentsRestController(InvestmentRepository investmentRepository) {
        this.investmentRepository = investmentRepository;
    }

    @RequestMapping(RequestMethod.POST)
    public List<Investment> update(@RequestBody List<Investment> investments) {
       return investmentRepository.update(investments);
    }

}

And the following json in the request body:

[
  {"investmentType":"Bond"},
  {"investmentType":"Equity"}
]

How can I bind or convert the json to a request body of List<Investment> without using Jackson's @JsonSubTypes on abstract class Investment, and instead use the InvestmentFactory?

like image 932
dukethrash Avatar asked Sep 12 '18 20:09

dukethrash


People also ask

Is @RequestBody required with @RestController?

If you don't add @RequestBody it will insert null values (should use), no need to use @ResponseBody since it's part of @RestController.

What is @RequestBody required false?

@RequestBody Body takes and argument required which is true by default. Specifying it to false will help you. public abstract boolean required. Whether body content is required. Default is true, leading to an exception thrown in case there is no body content.


2 Answers

@JsonDeserialize works great, but if you have more fields than just the type then you will have to set them all manually. If you were to go back to Jackson, you can use:

Investment.class

    @JsonTypeInfo(
            use = JsonTypeInfo.Id.NAME,
            include = JsonTypeInfo.As.PROPERTY,
            property = "investmentType")
    @JsonTypeIdResolver(InvestmentResolver.class)
    public abstract class Investment {
    } 

InvestmentResolver.class

public class InvestmentResolver extends TypeIdResolverBase {

    @Override
    public JavaType typeFromId(DatabindContext context, String id) throws IOException {
        Investment investment = InvestmentFactory.getTypeFromString(type);
        return context.constructType(investment.getClass());
    }

The beauty of this is that if you start adding fields to Investment you won't have to add them in the Desrializer (at least, this happened to me in my case), but instead Jackson will take care of it for you. So tomorrow you can have the test case:

'[{"investmentType":"Bond","investmentName":"ABC"},{"investmentType":"Equity","investmentName":"APPL"}]'

You should be good to go!

like image 160
Joe A Avatar answered Oct 17 '22 02:10

Joe A


If you can use @JsonDeserialize:

@JsonDeserialize(using = InvestmentDeserializer.class)
public abstract class Investment {
}



public class InvestmentDeserializer extends JsonDeserializer<Investment> {
    @Override
    public Investment deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
        TreeNode node = objectMapper.readTree(p);

        TreeNode investmentNode = node.get("investmentType");
        String type = objectMapper.readValue(investmentNode.traverse(objectMapper), String.class);
        return InvestmentFactory.getTypeFromString(type);
    }
}

Example controller:

@RestController
public class MyController {
    @RequestMapping("/")
    public List<Class<?>> update(@RequestBody List<Investment> investments) {
        return investments.stream().map(Object::getClass).collect(Collectors.toList());
    }
}

Testing:

$ curl localhost:8080 -H "Content-Type: application/json" -d '[{"investmentType":"Bond"},{"investmentType":"Equity"}]'

Output:

["com.example.demo.Bond","com.example.demo.Equity"]
like image 3
Denis Zavedeev Avatar answered Oct 17 '22 02:10

Denis Zavedeev