
We’ve all heard about the class Dog extends Animal
concept at some point of our wonderful programming lives, but have you ever heard about how to maintain such polymorphic structure when (de-)serializing the Dog class?
What is Polymorphism?
Theory
With subjects like this there is already a lot of information available on the world-wide web, and explaining polymorphism couldn’t be done better than how Torben Janssen described it on Stackify.com
Polymorphism describes situations in which something occurs in several different forms. In computer science, it describes the concept that you can access objects of different types through the same interface.
That last part is exactly what we want to achieve: deserializing a payload by specifying the interface without knowing the exact implementation that will be provided.
Example
Since you’ve heard the animal example a hundred times already, we’ll use a more real-life example and consider the following list of Messages.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[
{
"type": "TEXT",
"text": "Hello World!"
}, {
"type": "CONTACT",
"contact": {
"forename": "Joery",
"surname": "Vreijsen",
"phone": "+123456790"
}
}, {
"type": "LOCATION",
"location": {
"latitude": 50.894941,
"longitude": 4.341547
}
}
]
Implementation
With that we can start defining that interface from which we can access all different types of messages.
1
2
3
interface Message {
MessageType getType();
}
We can use the interface above that defines the message type, or we can define an abstract class like below to prevent duplicating constructor logic in every implementation.
1
2
3
4
5
6
7
8
9
10
11
abstract class Message {
protected MessageType type;
public Message(MessageType type) {
this.type = type;
}
public MessageType getType() {
return this.type;
}
}
By extending the abstract class we can now define our specific type of messages, starting with the obvious TextMessage.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TextMessage extends Message {
private final String text;
public TextMessage(@JsonProperty("text") String text) {
super(MessageType.TEXT);
this.text = text;
}
public String getText() {
return this.text;
}
}
Next to text messages we can also share locations, or contacts by sending LocationMessages or ContactMessages.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LocationMessage extends Message {
private final Location location;
public LocationMessage(@JsonProperty("location") Location location) {
super(MessageType.LOCATION);
this.location = location;
}
public Location getLocation() {
return this.location;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ContactMessage extends Message {
private final Contact contact;
public ContactMessage(@JsonProperty("contact") Contact contact) {
super(MessageType.CONTACT);
this.contact = contact;
}
public Contact getContact() {
return this.contact;
}
}
Alright, we have got all our models setup but we can’t quite deserialize the json from above yet because Jackson doesn’t know how to map a type to a specific class.
Here’s the trick; we can add the Jackson @JsonTypeInfo
annotation to specify that specific mapping on our Message interface or abstract class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = TextMessage.class, name = "TEXT"),
@JsonSubTypes.Type(value = LocationMessage.class, name = "LOCATION"),
@JsonSubTypes.Type(value = ContactMessage.class, name = "CONTACT")
})
public abstract class Message {
protected MessageType type;
public Message(MessageType type) {
this.type = type;
}
public MessageType getType() {
return this.type;
}
}
Let’s step back a bit shall we? We added the @JsonTypeInfo
specifying which property (type) should be used to descriminate the message.
We then add the @JsonSubTypes
annotation to specify the mapping of the property to the respective implementation.
As simple as that, we can now deserialize the json using Jackson’s ObjectMapper.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Main {
// Java 15+ Text Blocks 🎉
private static final String json =
"""
[
{
"type": "TEXT",
"text": "Hello World!"
}, {
"type": "CONTACT",
"contact": {
"forename": "Joery",
"surname": "Vreijsen",
"phone": "+123456790"
}
}, {
"type": "LOCATION",
"location": {
"latitude": 50.894941,
"longitude": 4.341547
}
}
]
""";
public static void main(String[] args) throws JsonProcessingException {
List<Message> messages = new ObjectMapper().readValue(json, new TypeReference<>() { });
System.out.println(messages);
}
}
Result
When we run this we see the following output confirming that we deserialized the json to our specific message classes.
1
[nl.vreijsenj.messaging.TextMessage@64485a47, nl.vreijsenj.messaging.ContactMessage@25bbf683, nl.vreijsenj.messaging.LocationMessage@6ec8211c]
Hooray! We could still extend our message class with properties like recipient
, sender
or add abstract methods for the specific classes to implement.