Let's say we have a class InstantMessage
, which has an abstract child IMContent content
.
public class InstantMessage {
// other fields
private MessageType type;
private IMContent content;
}
public enum MessageType {
TEXT, IMAGE, VIDEO, PRODUCT, SHOPPING_ORDER, SYSTEM;
}
public abstract class IMContent {
public abstract String toText();
}
IMContent
has a lot of implementations, like IMTextContent
, IMImageContent
, which depends on the type of message.
When using InstantMessage
in the @RequestBody
and @RequestPart
, Spring cannot deserialize the IMContent
.
I have tried several methods but they all have some obvious flaws.
Firstly, I tried adding @JsonDeserialize(using = IMContentDeserialzer.class)
to the content
, but that requires me to deserialize other fields manually besides content, which is every inconvenient and easy to forget when adding another field. I can accept that I write the deserialization of IMContent
manually but not the entire InstantMessage
. I wish there is a simple way to deserialize "non-content" fields.
My second attempt was using @JsonTypeInfo
and @JsonSubTypes
, but that seems to require me to move the type
from InstantMessage
to the IMContent
. It is also inconvenient when I need to access the type
from InstantMessage
directly or just access the text content directly. For example if I need to search the text content, I need to flatten the content into a String in the database and when I fetch the InstantMessage
out of db I need to resemble the content again.
I also have some workarounds, like using Map
instead of the IMContent
, or deserializing content into IMContent
on the fly, or making InstantMessage
abstract, but they are not elegant I think.
What's the easiest way to solve this problem in this case?
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = IMTextContent.class, name = "TEXT"),
@JsonSubTypes.Type(value = IMImageContent.class, name = "IMAGE"),
@JsonSubTypes.Type(value = IMProductContent.class, name = "PRODUCT"),
@JsonSubTypes.Type(value = IMShoppingOrderContent.class, name = "SHOPPING_ORDER"),
})
public abstract class IMContent {
public abstract String toText();
}
Usage of @JsonTypeInfo
and @JsonSubTypes
don't require moving the type from InstantMessage
to the IMContent
.
This requires telling how to distinguish between different types of IMContent
when deserializing, so you need to have some kind of property, like type
or whatever you want to call it, to associate it with a specific implementation of IMContent
class.
In other words, InstantMessage
can have a business! attribute MessageType type
property that describes the type of InstantMessage
, and IMContent
can have another type
property that describes the type of IMContent
in order to deserialize it.
But if MessageType type
describes from business point of view which content! this is a type, then fmpov it must belong to IMContent
and you don't need an InstantMessage
wrapper:
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = IMTextContent.class, name = "TEXT"),
@JsonSubTypes.Type(value = IMImageContent.class, name = "IMAGE"),
@JsonSubTypes.Type(value = IMProductContent.class, name = "PRODUCT"),
@JsonSubTypes.Type(value = IMShoppingOrderContent.class, name = "SHOPPING_ORDER"),
})
public abstract class IMContent {
protected MessageType type;
public abstract String toText();
// type getter/setter
}
public IMTextContent extends IMContent {
// further IMTextContent properties
// type is available via getter from IMContent
}
EDIT: You can achieve desired behaviour using JsonCreator:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class InstantMessage {
private Long id;
private MessageType type;
private IMContent imContent;
@JsonCreator
public InstantMessage(@JsonProperty("id") Long id,
@JsonProperty("type") MessageType type,
@JsonProperty("imContent") JsonNode imContent) {
this.id = id;
this.type = type;
this.imContent = deserializeContent(type, imContent);
}
// move this in separate class
private IMContent deserializeContent(MessageType type, JsonNode imContentNode) {
try {
if (type == MessageType.TEXT) {
String text = imContentNode.get("text").asText(); // specific property for IMTextContent
// further specific properties
IMTextContent imTextContent = new IMTextContent();
imTextContent.setText(text);
// set further specific properties
return imTextContent;
} else if (type == MessageType.IMAGE) {
String image = imContentNode.get("image").asText();
// extract further specific properties
IMImageContent imImageContent = new IMImageContent();
imImageContent.setImage(image);
// set further specific properties
return imImageContent;
}
} catch (Exception e) {
throw new RuntimeException(e); // handle failed to deserialize
}
return null; // also handle case if message type unknown
}
public static void main(String[] args) throws JsonProcessingException {
String json = """
{
"id": "44",
"type":"TEXT",
"imContent": {
"text": "bla"
}
}
""";
InstantMessage instantMessage = new ObjectMapper().readValue(json, InstantMessage.class);
System.out.println(instantMessage); // InstantMessage(id=44, type=TEXT, imContent=IMTextContent(text=bla))
System.out.println(instantMessage.getImContent().getClass().getSimpleName()); // IMTextContent
}
}