Default subtype in @JsonbTypeInfo (for polymorphism deserialization)
With @JsonbTypeInfo and @JsonbSubtype it is possible to implement polymorphism on deserialization.
But sometimes (especially in the case of the evolution of a REST API over time) the attribute corresponding to the defined key might not be present in the provided JSON message.
For this use-case, being able to specify the "default subtype" would be necessary.
Currently yasson is failing with this error:
jakarta.json.bind.JsonbException: Cannot infer a type for unmarshalling into: snippet.Snippet$Animal
Complete example:
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbException;
import jakarta.json.bind.annotation.JsonbSubtype;
import jakarta.json.bind.annotation.JsonbTypeInfo;
public class Snippet {
@JsonbTypeInfo({
@JsonbSubtype(alias = "dog", type = Dog.class),
@JsonbSubtype(alias = "cat", type = Cat.class)
})
public static interface Animal {
}
public static final class Dog implements Animal {
public boolean isDog = true;
@Override
public String toString() {
return "Dog [isDog=" + isDog + "]";
}
}
public static final class Cat implements Animal {
public boolean isCat = true;
@Override
public String toString() {
return "Cat [isCat=" + isCat + "]";
}
}
public static void main(String[] args) {
// Create a Jsonb instance
Jsonb jsonb = JsonbBuilder.create();
// Create instances of Dog and Cat
Dog myDog = new Dog();
Cat myCat = new Cat();
// Serialize Dog instance to JSON
try {
String dogJson = jsonb.toJson(myDog);
System.out.println("Serialized Dog JSON: " + dogJson);
// Serialize Cat instance to JSON
String catJson = jsonb.toJson(myCat);
System.out.println("Serialized Cat JSON: " + catJson);
// Deserialize JSON (polymorphic deserialization) Dog
Animal deserializedAnimalFromDogJson = jsonb.fromJson("{\"@type\":\"dog\"}", Animal.class);
System.out.println("Deserialized Dog: " + deserializedAnimalFromDogJson);
System.out.println("Deserialized Animal from Dog (instance of check): " + (deserializedAnimalFromDogJson instanceof Dog));
// Deserialize JSON (polymorphic deserialization) Cat
Animal deserializedAnimalFromCatJson = jsonb.fromJson("{\"@type\":\"cat\"}", Animal.class);
System.out.println("Deserialized Cat: " + deserializedAnimalFromCatJson);
System.out.println("Deserialized Animal from Cat (instance of check): " + (deserializedAnimalFromCatJson instanceof Cat));
// Deserialize JSON (default impl)
Animal a = jsonb.fromJson("{}", Animal.class);
System.out.println("Deserialized Animal: " + a);
} catch (JsonbException e) {
e.printStackTrace();
}
}
}
This is the part of the spec that defines this behavior, IMO one could read this as "throw an exception if the defined alias is present and doesn't match any of the defined subtypes"
If no matching class is found for obtained alias during deserialization, an exception must be thrown.
This is also how we implemented polymorphism in Apache Johnzon, running your example yields: jakarta.json.bind.JsonbException: Animal is an interface and requires an adapter or factory. Cannot deserialize json object value: {}
Johnzon just does not know how to instantiate the interface. Yasson actually also works this way, the exception message is just not 100% clear about this.
For example, this works in both Johnzon and Yasson
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbException;
import jakarta.json.bind.annotation.JsonbCreator;
import jakarta.json.bind.annotation.JsonbProperty;
import jakarta.json.bind.annotation.JsonbSubtype;
import jakarta.json.bind.annotation.JsonbTypeInfo;
public class Snippet {
@JsonbTypeInfo({
@JsonbSubtype(alias = "dog", type = Dog.class),
@JsonbSubtype(alias = "cat", type = Cat.class)
})
public static interface Animal {
@JsonbCreator
static Animal create(@JsonbProperty("isDefault") boolean isDefault)
{
DefaultAnimal defaultAnimal = new DefaultAnimal();
defaultAnimal.isDefault = isDefault;
return defaultAnimal;
}
static class DefaultAnimal implements Animal {
public boolean isDefault;
@Override
public String toString() {
return "DefaultAnimal [isDefault=" + isDefault + "]";
}
}
}
public static final class Dog implements Animal {
public boolean isDog = true;
@Override
public String toString() {
return "Dog [isDog=" + isDog + "]";
}
}
public static final class Cat implements Animal {
public boolean isCat = true;
@Override
public String toString() {
return "Cat [isCat=" + isCat + "]";
}
}
public static void main(String[] args) {
// Create a Jsonb instance
Jsonb jsonb = JsonbBuilder.create();
// Create instances of Dog and Cat
Dog myDog = new Dog();
Cat myCat = new Cat();
// Serialize Dog instance to JSON
try {
String dogJson = jsonb.toJson(myDog);
System.out.println("Serialized Dog JSON: " + dogJson);
// Serialize Cat instance to JSON
String catJson = jsonb.toJson(myCat);
System.out.println("Serialized Cat JSON: " + catJson);
// Deserialize JSON (polymorphic deserialization) Dog
Animal deserializedAnimalFromDogJson = jsonb.fromJson("{\"@type\":\"dog\"}", Animal.class);
System.out.println("Deserialized Dog: " + deserializedAnimalFromDogJson);
System.out.println("Deserialized Animal from Dog (instance of check): " + (deserializedAnimalFromDogJson instanceof Dog));
// Deserialize JSON (polymorphic deserialization) Cat
Animal deserializedAnimalFromCatJson = jsonb.fromJson("{\"@type\":\"cat\"}", Animal.class);
System.out.println("Deserialized Cat: " + deserializedAnimalFromCatJson);
System.out.println("Deserialized Animal from Cat (instance of check): " + (deserializedAnimalFromCatJson instanceof Cat));
// Deserialize JSON (default impl)
Animal a = jsonb.fromJson("{\"isDefault\":true}", Animal.class);
System.out.println("Deserialized Animal: " + a);
} catch (JsonbException e) {
e.printStackTrace();
}
}
}
This is more of a hack though, so I agree this is something that should be put in the spec somewhere (maybe something like @JsonbSubtype(alias = JsonbSubtype.NONE, type=...))
I am not implying that something is not implemented according to spec, I think a feature is missing.
Imaging you implement the server-side of an endpoint accepting person as request-body:
{
"fistName":"John",
"lastName":"Doe"
}
Then you do an API evolution to support different type of persons: artists and employees.
{
"@type": "artist"
"fistName":"John",
"lastName":"Doe",
"skills": ["painting"]
}
{
"@type": "employee"
"fistName":"Bob",
"lastName":"Smith",
"jobTitle": "Developer",
"department", "R&D"
}
The implementation with @JsonbTypeInfo is straight forward.
With the current state of the spec I don't see how you can implement following use-cases:
- You want to stay backward-compatible and still support deserialization of JSON message without the
@typeattribute.
You could decide to deserialize those as Artist (where skills would be null) if this makes sense for your business logic.
Or having a third class UnknownPerson implementing the Person interface (next to Artist and Employee that are registered using @JsonbSubtype).
- You want to have a robust implementation and support deserialization of unknown discriminator value like
visitor:
{
"@type": "visitor"
"fistName":"Thomas",
"lastName":"Spencer",
"badge": "FFA7E"
}
The idea would be that your app should deserialize this JSON document (for example as UnknownPerson) until you have time to modify your code and add a third @JsonbSubtype.
I think supporting those use cases requires being able to support a default subtype.
I'm not arguing against your proposal, I was just thinking out lout what is already possible (with arguably questionable usage of JsonbCreator) right now
I think at least having a "default subtype" would make a lot of sense, and maybe a config option could be added to handle unknown discriminators more gracefully. The spec is pretty strict in its current version on that implementations must throw an exception if an unknown type is encountered during mapping
With the current state of the spec I don't see how you can implement following use-cases:
You actually have several options:
- at jsonp level you preprocess the input ensuring there is a "@type" entry and you pass your custom parser/reader to jsonb
- you just use a custom (de)serializer or adapter
- it is also possible to decorate Jsonb API to do it (even if I wouldn't do it at that level it works very well for that use case as well - ie Reader/InputStream at the end)
Long story short the polyphormic API is totally useless to support polymorphic use cases, it is just convenient for some particular modellings. I'm not convinced making them ultra generic would be good cause it would become overcomplex for a very limited gain. Default subtype is an interesting use case because it is linked to default/implicit values and as of today it is out of scope - except for primitives due to technical limitations - so I do not think we should make it specific to polymorphism (and ultimately I do not think it should land to the spec to keep user codes simple).