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
?
If you don't add @RequestBody it will insert null values (should use), no need to use @ResponseBody since it's part of @RestController.
@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.
@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!
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"]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With