Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is Jackson's @JsonSubTypes still necessary for polymorphic deserialization?

I am able to serialize and deserialize a class hierarchy where the abstract base class is annotated with

@JsonTypeInfo(     use = JsonTypeInfo.Id.MINIMAL_CLASS,     include = JsonTypeInfo.As.PROPERTY,     property = "@class") 

but no @JsonSubTypes listing the subclasses, and the subclasses themselves are relatively unannotated, having only a @JsonCreator on the constructor. The ObjectMapper is vanilla, and I'm not using a mixin.

Jackson documentation on PolymorphicDeserialization and "type ids" suggests (strongly) I need the @JsonSubTypes annotation on the abstract base class, or use it on a mixin, or that I need to register the subtypes with the ObjectMapper. And there are plenty of SO questions and/or blog posts that agree. Yet it works. (This is Jackson 2.6.0.)

So ... am I the beneficiary of an as-yet-undocumented feature or am I relying on undocumented behavior (that may change) or is something else going on? (I'm asking because I really don't want it to be either of the latter two. But I gots to know.)

EDIT: Adding code - and one comment. The comment is: I should have mentioned that all the subclasses I'm deserializing are in the same package and same jar as the base abstract class.

Abstract base class:

package so; import com.fasterxml.jackson.annotation.JsonTypeInfo;  @JsonTypeInfo(     use = JsonTypeInfo.Id.MINIMAL_CLASS,     include = JsonTypeInfo.As.PROPERTY,     property = "@class") public abstract class PolyBase {     public PolyBase() { }      @Override     public abstract boolean equals(Object obj); } 

A subclass of it:

package so; import org.apache.commons.lang3.builder.EqualsBuilder; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty;  public final class SubA extends PolyBase {     private final int a;      @JsonCreator     public SubA(@JsonProperty("a") int a) { this.a = a; }      public int getA() { return a; }      @Override     public boolean equals(Object obj) {         if (null == obj) return false;         if (this == obj) return true;         if (this.getClass() != obj.getClass()) return false;          SubA rhs = (SubA) obj;         return new EqualsBuilder().append(this.a, rhs.a).isEquals();     } } 

Subclasses SubB and SubC are the same except that field a is declared String (not int) in SubB and boolean (not int) in SubC (and the method getA is modified accordingly).

Test class:

package so;     import java.io.IOException; import org.apache.commons.lang3.builder.EqualsBuilder; import org.testng.annotations.Test; import static org.assertj.core.api.Assertions.*; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper;  public class TestPoly {     public static class TestClass     {         public PolyBase pb1, pb2, pb3;          @JsonCreator         public TestClass(@JsonProperty("pb1") PolyBase pb1,                          @JsonProperty("pb2") PolyBase pb2,                          @JsonProperty("pb3") PolyBase pb3)         {             this.pb1 = pb1;             this.pb2 = pb2;             this.pb3 = pb3;         }          @Override         public boolean equals(Object obj) {             if (null == obj) return false;             if (this == obj) return true;             if (this.getClass() != obj.getClass()) return false;              TestClass rhs = (TestClass) obj;             return new EqualsBuilder().append(pb1, rhs.pb1)                                       .append(pb2, rhs.pb2)                                       .append(pb3, rhs.pb3)                                       .isEquals();         }     }      @Test     public void jackson_should_or_should_not_deserialize_without_JsonSubTypes() {          // Arrange         PolyBase pb1 = new SubA(5), pb2 = new SubB("foobar"), pb3 = new SubC(true);         TestClass sut = new TestClass(pb1, pb2, pb3);          ObjectMapper mapper = new ObjectMapper();          // Act         String actual1 = null;         TestClass actual2 = null;          try {             actual1 = mapper.writeValueAsString(sut);         } catch (IOException e) {             fail("didn't serialize", e);         }          try {             actual2 = mapper.readValue(actual1, TestClass.class);         } catch (IOException e) {             fail("didn't deserialize", e);         }          // Assert         assertThat(actual2).isEqualTo(sut);     } } 

This test passes and if you break at the second try { line you can inspect actual1 and see:

{"pb1":{"@class":".SubA","a":5},  "pb2":{"@class":".SubB","a":"foobar"},  "pb3":{"@class":".SubC","a":true}} 

So the three subclasses got properly serialized (each with their class name as id) and then deserialized, and the result compared equal (each subclass has a "value type" equals()).

like image 731
davidbak Avatar asked Jul 28 '15 00:07

davidbak


People also ask

What is the use of @JsonProperty?

@JsonProperty is used to mark non-standard getter/setter method to be used with respect to json property.

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.

What is the use of JsonCreator?

@JsonCreator is used to fine tune the constructor or factory method used in deserialization. We'll be using @JsonProperty as well to achieve the same. In the example below, we are matching an json with different format to our class by defining the required property names.


1 Answers

There are two ways to achieve polymorphism in serialization and deserialization with Jackson. They are defined in Section 1. Usage in the link you posted.

Your code

@JsonTypeInfo(     use = JsonTypeInfo.Id.MINIMAL_CLASS,     include = JsonTypeInfo.As.PROPERTY,     property = "@class") 

is an example of the second approach. The first thing to note is that

All instances of annotated type and its subtypes use these settings (unless overridden by another annotation)

So this config value propagates to all subtypes. Then, we need a type identifier that will map a Java type to a text value in the JSON string and vice versa. In your example, this is given by JsonTypeInfo.Id#MINIMAL_CLASS

Means that Java class name with minimal path is used as the type identifier.

So a minimal class name is generated from the target instance and written to the JSON content when serializing. Or a minimal class name is used to determine the target type for deserializing.

You could have also used JsonTypeInfo.Id#NAME which

Means that logical type name is used as type information; name will then need to be separately resolved to actual concrete type (Class).

To provide such a logical type name, you use @JsonSubTypes

Annotation used with JsonTypeInfo to indicate sub types of serializable polymorphic types, and to associate logical names used within JSON content (which is more portable than using physical Java class names).

This is just another way to achieve the same result. The documentation you're asking about states

Type ids that are based on Java class name are fairly straight-forward: it's just class name, possibly some simple prefix removal (for "minimal" variant). But type name is different: one has to have mapping between logical name and actual class.

So the various JsonTypeInfo.Id values that deal with class names are straight-forward because they can be auto-generated. For type names, however, you need to give the mapping value explicitly.

like image 52
Sotirios Delimanolis Avatar answered Sep 28 '22 02:09

Sotirios Delimanolis