Home » Java » REST How to implement polymorphic POST end point: abstract types need to be mapped to concrete types

REST How to implement polymorphic POST end point: abstract types need to be mapped to concrete types

Posted by: admin December 28, 2021 Leave a comment

Questions:

I work on a SpringBoot application.

I have the following class hierarchy:

public abstract class DlqMessage {
    Long id;
    UUID employeeId;
    EventReason reason;
}

public class ContractUpdateMessage extends DlqMessage {}

public class SingleContractUpdateMessage extends DlqMessage {
    UUID benefitId;
    UUID employerId;
}

So, the classes ContractUpdateMessage and SingleContractUpdateMessage only differ by a couple of fields.

I Have a REST controller that uses POST to create and save a new entity in the DB:

@PostMapping("/messages")
public ResponseEntity<DlqMessage> create(@RequestBody DlqMessage contractUpdateMessage) {
    DlqMesssage dlqEntry = dlqEntityService.save(contractUpdateMessage);
    return ResponseEntity.ok(dlqEntry);
}

Now, I have a test that randomly generates on instance of the one or the other class:

 DlqMessage message = randomOneOf(contractUpdateMessageBuilder().build(), singleContractUpdateMessageBuilder().build());

Then I have a test helper class that uses RestTemplate to send a POST request to the controller:

ResponseEntity<DlqMesssage> response =
                crudRestClient.post("/messages/contract", message, DlqMesssage.class, null);
        return response.getBody();

And invoking the whole things I end up with the following exception:

Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Can not construct instance of com.orbitbenefits.benefitsselection.server.errorrecovery.entity.DlqMessage: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.orbitbenefits.benefitsselection.server.errorrecovery.entity.DlqMessage: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

And it looks like my request isn’t even being sent by the RestTemplate.

BTW, it all works when I split endpoints for each individual subtype:

@PostMapping("/messages/contract")
public ResponseEntity<DlqMessage> create(@RequestBody ContractUpdateMessage contractUpdateMessage) {
    ContractUpdateMessage dlqEntry = (ContractUpdateMessage) dlqEntityService.save(contractUpdateMessage);
    return ResponseEntity.ok(dlqEntry);
}

@PostMapping("/messages/single")
public ResponseEntity<DlqMessage> create(@RequestBody SingleContractUpdateMessage contractUpdateMessage) {
    SingleContractUpdateMessage dlqEntry = (SingleContractUpdateMessage) dlqEntityService.save(contractUpdateMessage);
    return ResponseEntity.ok(dlqEntry);
}

However that looks ugly and is not a “correct” solution.

Basically, I would like to know if it’s possible and how to implement a REST end point that takes polymorphic instance as a parameter and how to invoke such an end point?

Answers:

The reason you can’t use the abstract type DlqMessage is because you may receive a message like:

{
  "id":300, 
  "employeeId":"d6a00fb2-058c-4bf7-9a0a-7cc538cd85f5"
}

Jackson can’t determine which is type of the concrete object that this message is intending to map. The simplest way to handle this is defining a type hint, like:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "messageType")
@JsonSubTypes({
    @JsonSubTypes.Type(value=ContractUpdateMessage.class, name = "ContractUpdateMessage"),
    @JsonSubTypes.Type(value=SingleContractUpdateMessage.class, name = "SingleContractUpdateMessage")
})
public abstract class DlqMessage { ... }

This way, the next time you make an API call you must also include this type hint in your JSON object:

{
  "id":300, 
  "employeeId":"d6a00fb2-058c-4bf7-9a0a-7cc538cd85f5",
  "messageType":"ContractUpdateMessage"
}

This way, Jackson’s default object mapper will use the field “messageType” to guess which type of DlqMessage your API is receiving.

Edit: You can find further information here:

https://www.baeldung.com/jackson-annotations

###

The issue is that spring is trying to create an instance of DlqMessage directly from the input payload. Assuming your API accepts JSON input, you’ll need to instruct your json parser on how to distinguish between the subtypes based on payload contents.

Here’s a link to a similar question that shows an example of how to do so with jackson json. Looks like you’ll have to annotate the abstract class with knowledge of the subtypes via @JsonSubTypes as well as provide the distinguishing fields via @JsonTypeInfo:

https://stackoverflow.com/a/27183383/1563240

###

you can you jackson mapping to map the classes. Add a type property in the class.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "type")
@JsonSubTypes({@JsonSubTypes.Type(value = ContractUpdateMessage.class, name = "ContractUpdateMessage"),             
               @JsonSubTypes.Type(value = SingleContractUpdateMessage.class, name = "SingleContractUpdateMessage")})
public abstract class DlqMessage {