Can Jackson polymorphic deserialization be used to serialize to a subtype if a specific field is present? Can Jackson polymorphic deserialization be used to serialize to a subtype if a specific field is present? json json

Can Jackson polymorphic deserialization be used to serialize to a subtype if a specific field is present?


Jackson 2.12 added "deduction-based polymorphism" to automatically deduce subtypes based on the presence of properties distinct to a particular subtype. As of Jackson 2.12.2, it is possible to specify a type to be used when there isn't a subtype uniquely identifiable by the subtype-specific properties.

These features can be used to accomplish the requested deserialization in Jackson 2.12.2 or later. To do so, use @JsonTypeInfo(use=Id.DEDUCTION, defaultImpl = Animal.class) alongside the full list of supported subtypes provided by @JsonSubTypes:

@JsonTypeInfo(use=Id.DEDUCTION, defaultImpl = Animal.class)@JsonSubTypes({@Type(Bird.class)})public class Animal {    public String name;    public int age;}

Deduction-based polymorphism

The deduction-based polymorphism feature was implemented per jackson-databind#43, 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 behavior is improved 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.


While not directly answering your question, I did think it was worth pointing out that it's not overly burdensome to use @JsonCreator:

@JsonCreatorpublic static Animal create(Map<String,Object> jsonMap) {    String name = (String) jsonMap.get("name");    int age = (int) jsonMap.get("age");    if (jsonMap.keySet().contains("wingspan")) {        double wingspan = (double) jsonMap.get("wingspan");        return new Bird(name, age, wingspan);    } else {        return new Animal(name, age);    }}

No need to throw JsonProcessingException. This custom deserializer would fail for exactly the same reasons that the built-in Jackson deserializer would, namely, casting exceptions. For complex deserialization I prefer this way of doing things, as it makes the code much easier to understand and modify.


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

  1. Use Jackson to deserialize the input into a general JsonNode
  2. Use https://github.com/json-path/JsonPath check for one or more properties existence. Some container class could wrap all the paths needed to uniquely identify a class type.
  3. Map the JsonNode to the determined class, as outlined here Convert JsonNode into POJO

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);    }}