Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 752
Shaun Avatar asked May 10 '13 18:05

Shaun


People also ask

How do you tell Jackson to ignore a field during serialization?

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.

What is polymorphic 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.

Does Jackson serialize getters?

Jackson doesn't (by default) care about fields. It will simply serialize everything provided by getters and deserialize everything with a matching setter.

What is Jackson object serialization?

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.


2 Answers

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

Deduction-based polymorphism

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.

like image 79
M. Justin Avatar answered Sep 19 '22 12:09

M. Justin


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);     } } 
like image 23
Marius Schmidt Avatar answered Sep 19 '22 12:09

Marius Schmidt