Using a spin on the zoo example:
public class ZooPen { public String type; public List<Animal> animals; } public class Animal { public String name; public int age; } public class Bird extends Animal { public double wingspan; }
I want to use polymorphic deserialization to construct Animal
instances if no wingspan is specified, and Bird
instances if it is. In Jackson, untyped deserialization typically looks something like this:
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "wingspan", visible = true, defaultImpl = Animal.class ) @JsonSubTypes({ @Type(value = Bird.class, name = "bird") }) public class Animal { ... }
The wingspan value can be anything, and without that matching something specifically, Jackson falls back on the defaultImpl class.
I could probably use @JsonCreator
:
@JsonCreator public static Animal create(Map<String,Object> jsonMap) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); if (jsonMap.get("wingspan") == null) { // Construct and return animal } else { // Construct and return bird } }
However, then I have to manually handle extra values and throw consistent exceptions, and it's not clear if the Animal
would be serialized properly later.
It seems I can use my own TypeResolver
or TypeIdResolver
, but that seems like more work than just deserializing the raw json myself. Additionally, TypeResolver
and TypeIdResolver
seem to intrinsically assume that type info is serialized, so those aren't good to use.
Would it be feasible to implement my own JsonDeserializer
that hooks into the lifecycle to specify type, but still uses basic Jackson annotation processing functionality? I've been having a look at JsonDeserializer.deserializeWithType(...)
, but that seems to delegate deserialization entirely to a TypeDeserializer
. There's also the issue that I'd need to deserialize some of the object before I know which type to use.
Alternatively, there might be a way to target the type of zoo pen, even though it's in the parent object.
Is there a way to do what I want with polymorphic type handling?
If there are fields in Java objects that do not wish to be serialized, we can use the @JsonIgnore annotation in the Jackson library. The @JsonIgnore can be used at the field level, for ignoring fields during the serialization and deserialization.
A polymorphic deserialization allows a JSON payload to be deserialized into one of the known gadget classes that are documented in SubTypeValidator. java in jackson-databind in GitHub. The deserialized object is assigned to a generic base class in your object model, such as java. lang.
Jackson doesn't (by default) care about fields. It will simply serialize everything provided by getters and deserialize everything with a matching setter.
Jackson is a solid and mature JSON serialization/deserialization library for Java. The ObjectMapper API provides a straightforward way to parse and generate JSON response objects with a lot of flexibility.
As of Jackson 2.12.2, the following accomplishes the goal using the "deduction-based polymorphism" feature. If properties distinct to the Bird
subtype (i.e. wingspan
) are present, the deserialized type will be Bird
; else it will be Animal
:
@JsonTypeInfo(use=Id.DEDUCTION, defaultImpl = Animal.class) @JsonSubTypes({@Type(Bird.class)}) public class Animal { public String name; public int age; }
The deduction-based polymorphism feature deduces subtypes based on the presence of properties distinct to a particular subtype. If there isn't a subtype uniquely identifiable by the subtype-specific properties, the type specified by defaultImpl
value will be used.
The deduction-based polymorphism feature was implemented per jackson-databind#43 in Jackson 2.12, and is summarized in the 2.12 release notes:
It basically allows omitting of actual Type Id field or value, as long as the subtype can be deduced (
@JsonTypeInfo(use=DEDUCTION)
) from existence of fields. That is, every subtype has a distinct set of fields they included, and so during deserialization type can be uniquely and reliably detected.
This ability to specify a default type — rather than throw an exception — when there is no uniquely identifiable subtype was added by jackson-databind#3055 in Jackson 2.12.2:
In the absence of a single candidate,
defaultImpl
should be the target type regardless of suitability.
A slightly longer explanation of deduction-based polymorphism is given in the Jackson 2.12 Most Wanted (1/5): Deduction-Based Polymorphism article written by the Jackson creator.
EDIT: If you can use the latest Jackson release candidate, your problem is solved. I assembled a quick demo here https://github.com/MariusSchmidt/de.denktmit.stackoverflow/tree/main/de.denktmit.jackson
You should take a look at this thread https://github.com/FasterXML/jackson-databind/issues/1627, as it discusses your problem and proposes a solution. There is a Merge, that looks promising to me https://github.com/FasterXML/jackson-databind/pull/2813. So you might try to follow the path of @JsonTypeInfo(use = DEDUCTION).
If however you can not use the latest upcoming Jackson version, here is what I would likely do:
Backport the merge request, OR
This way, you can leverage the full power of Jackson without handling low-level mapping logic
Best regards,
Marius
Animal
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.ObjectMapper; import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Bird; import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Fish; import org.junit.jupiter.api.Test; import java.util.List; import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.DEDUCTION; import static org.assertj.core.api.Assertions.assertThat; @JsonTypeInfo(use = DEDUCTION) @JsonSubTypes( {@JsonSubTypes.Type(Bird.class), @JsonSubTypes.Type(Fish.class)}) public class Animal { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
Bird
public class Bird extends de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal { private double wingspan; public double getWingspan() { return wingspan; } public void setWingspan(double wingspan) { this.wingspan = wingspan; } }
Fish
public class Fish extends de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal { private boolean freshwater; public boolean isFreshwater() { return freshwater; } public void setFreshwater(boolean freshwater) { this.freshwater = freshwater; } }
ZooPen
public class ZooPen { private String type; private List<de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal> animals; public String getType() { return type; } public void setType(String type) { this.type = type; } public List<de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal> getAnimals() { return animals; } public void setAnimals(List<de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal> animals) { this.animals = animals; } }
The test
import com.fasterxml.jackson.databind.ObjectMapper; import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal; import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Bird; import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Fish; import de.denktmit.stackoverflow.jackson.polymorphic.deductive.ZooPen; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class DeductivePolymorphicDeserializationTest { private static final String birdString = "{\n" + " \"name\": \"Tweety\",\n" + " \"age\": 79,\n" + " \"wingspan\": 2.9\n" + " }"; private static final String fishString = "{\n" + " \"name\": \"Nemo\",\n" + " \"age\": 16,\n" + " \"freshwater\": false\n" + " }"; private static final String zooPenString = "{\n" + " \"type\": \"aquaviary\",\n" + " \"animals\": [\n" + " {\n" + " \"name\": \"Tweety\",\n" + " \"age\": 79,\n" + " \"wingspan\": 2.9\n" + " },\n" + " {\n" + " \"name\": \"Nemo\",\n" + " \"age\": 16,\n" + " \"freshwater\": false\n" + " }\n" + " ]\n" + "}"; private final ObjectMapper mapper = new ObjectMapper(); @Test void deserializeBird() throws Exception { de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal animal = mapper.readValue(birdString, de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal.class); assertThat(animal).isInstanceOf(de.denktmit.stackoverflow.jackson.polymorphic.deductive.Bird.class); } @Test void deserializeFish() throws Exception { de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal animal = mapper.readValue(fishString, de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal.class); assertThat(animal).isInstanceOf(de.denktmit.stackoverflow.jackson.polymorphic.deductive.Fish.class); } @Test void deserialize() throws Exception { de.denktmit.stackoverflow.jackson.polymorphic.deductive.ZooPen zooPen = mapper.readValue(zooPenString, de.denktmit.stackoverflow.jackson.polymorphic.deductive.ZooPen.class); assertThat(zooPen).isInstanceOf(de.denktmit.stackoverflow.jackson.polymorphic.deductive.ZooPen.class); } }
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