Warisan dengan Jackson

1. Ikhtisar

Pada artikel ini, kita akan melihat cara bekerja dengan hierarki kelas di Jackson.

Dua kasus penggunaan tipikal adalah penyertaan metadata subtipe dan mengabaikan properti yang diwarisi dari superclass. Kami akan menjelaskan dua skenario tersebut dan beberapa keadaan di mana perlakuan khusus untuk subtipe diperlukan.

2. Pencantuman Informasi Subjenis

Ada dua cara untuk menambahkan informasi tipe saat membuat serialisasi dan deserialisasi objek data, yaitu pengetikan default global dan anotasi per kelas.

2.1. Pengetikan Default Global

Tiga kelas Java berikut akan digunakan untuk menggambarkan penyertaan global metadata jenis.

Superkelas kendaraan :

public abstract class Vehicle { private String make; private String model; protected Vehicle(String make, String model) { this.make = make; this.model = model; } // no-arg constructor, getters and setters }

Subkelas mobil :

public class Car extends Vehicle { private int seatingCapacity; private double topSpeed; public Car(String make, String model, int seatingCapacity, double topSpeed) { super(make, model); this.seatingCapacity = seatingCapacity; this.topSpeed = topSpeed; } // no-arg constructor, getters and setters }

Subkelas truk :

public class Truck extends Vehicle { private double payloadCapacity; public Truck(String make, String model, double payloadCapacity) { super(make, model); this.payloadCapacity = payloadCapacity; } // no-arg constructor, getters and setters }

Pengetikan default global memungkinkan informasi tipe untuk dideklarasikan hanya sekali dengan mengaktifkannya pada objek ObjectMapper . Metadata jenis itu kemudian akan diterapkan ke semua jenis yang ditentukan. Hasilnya, akan sangat mudah menggunakan metode ini untuk menambahkan metadata tipe, terutama bila ada banyak tipe yang terlibat. Sisi negatifnya adalah ia menggunakan nama tipe Java yang memenuhi syarat sebagai pengenal tipe, dan karenanya tidak cocok untuk interaksi dengan sistem non-Java, dan hanya berlaku untuk beberapa tipe tipe yang telah ditentukan sebelumnya.

The Kendaraan struktur yang ditunjukkan di atas digunakan untuk mengisi sebuah instance dari Armada kelas:

public class Fleet { private List vehicles; // getters and setters }

Untuk menyematkan metadata tipe, kita perlu mengaktifkan fungsionalitas pengetikan pada objek ObjectMapper yang akan digunakan untuk serialisasi dan deserialisasi objek data nanti:

ObjectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

The penerapan parameter menentukan jenis yang membutuhkan informasi jenis, dan includeAs parameter adalah mekanisme untuk jenis metadata inklusi. Selain itu, tersedia dua varian lain dari metode enableDefaultTyping :

  • ObjectMapper.enableDefaultTyping (Penerapan ObjectMapper.DefaultTyping) : memungkinkan pemanggil untuk menentukan penerapan , sambil menggunakan WRAPPER_ARRAY sebagai nilai default untuk includeAs
  • ObjectMapper.enableDefaultTyping (): menggunakan OBJECT_AND_NON_CONCRETE sebagai nilai default untuk penerapan dan WRAPPER_ARRAY sebagai nilai default untuk includeAs

Mari kita lihat cara kerjanya. Untuk memulai, kita perlu membuat objek ObjectMapper dan mengaktifkan pengetikan default di atasnya:

ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping();

Langkah selanjutnya adalah membuat instance dan mengisi struktur data yang diperkenalkan di awal sub-bagian ini. Kode untuk melakukannya akan digunakan kembali nanti di sub-bagian berikutnya. Demi kenyamanan dan penggunaan kembali, kami akan menamakannya blok instansiasi kendaraan .

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0); Truck truck = new Truck("Isuzu", "NQR", 7500.0); List vehicles = new ArrayList(); vehicles.add(car); vehicles.add(truck); Fleet serializedFleet = new Fleet(); serializedFleet.setVehicles(vehicles);

Objek yang terisi tersebut kemudian akan dibuat serial:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

String JSON yang dihasilkan:

{ "vehicles": [ "java.util.ArrayList", [ [ "org.baeldung.jackson.inheritance.Car", { "make": "Mercedes-Benz", "model": "S500", "seatingCapacity": 5, "topSpeed": 250.0 } ], [ "org.baeldung.jackson.inheritance.Truck", { "make": "Isuzu", "model": "NQR", "payloadCapacity": 7500.0 } ] ] ] }

Selama deserialisasi, objek dipulihkan dari string JSON dengan jenis data yang dipertahankan:

Fleet deserializedFleet = mapper.readValue(jsonDataString, Fleet.class);

Objek yang dibuat ulang akan menjadi subtipe konkret yang sama seperti sebelum serialisasi:

assertThat(deserializedFleet.getVehicles().get(0), instanceOf(Car.class)); assertThat(deserializedFleet.getVehicles().get(1), instanceOf(Truck.class));

2.2. Anotasi Per-Kelas

Anotasi per kelas adalah metode yang ampuh untuk menyertakan informasi tipe dan bisa sangat berguna untuk kasus penggunaan yang kompleks di mana tingkat penyesuaian yang signifikan diperlukan. Namun, ini hanya dapat dicapai dengan mengorbankan komplikasi. Anotasi per kelas menimpa pengetikan default global jika informasi jenis dikonfigurasi dengan kedua cara.

Untuk menggunakan metode ini, supertipe harus dianotasi dengan @JsonTypeInfo dan beberapa anotasi relevan lainnya. Subbagian ini akan menggunakan model data yang mirip dengan struktur Kendaraan pada contoh sebelumnya untuk mengilustrasikan anotasi per kelas. Satu-satunya perubahan adalah penambahan anotasi pada kelas abstrak Kendaraan , seperti yang ditunjukkan di bawah ini:

@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes({ @Type(value = Car.class, name = "car"), @Type(value = Truck.class, name = "truck") }) public abstract class Vehicle { // fields, constructors, getters and setters }

Objek data dibuat menggunakan blok instansiasi kendaraan yang diperkenalkan di subbagian sebelumnya, dan kemudian dibuat serial:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

Serialisasi menghasilkan struktur JSON berikut:

{ "vehicles": [ { "type": "car", "make": "Mercedes-Benz", "model": "S500", "seatingCapacity": 5, "topSpeed": 250.0 }, { "type": "truck", "make": "Isuzu", "model": "NQR", "payloadCapacity": 7500.0 } ] }

String itu digunakan untuk membuat ulang objek data:

Fleet deserializedFleet = mapper.readValue(jsonDataString, Fleet.class);

Akhirnya, seluruh kemajuan divalidasi:

assertThat(deserializedFleet.getVehicles().get(0), instanceOf(Car.class)); assertThat(deserializedFleet.getVehicles().get(1), instanceOf(Truck.class));

3. Mengabaikan Properti dari Supertipe

Sometimes, some properties inherited from superclasses need to be ignored during serialization or deserialization. This can be achieved by one of three methods: annotations, mix-ins and annotation introspection.

3.1. Annotations

There are two commonly used Jackson annotations to ignore properties, which are @JsonIgnore and @JsonIgnoreProperties. The former is directly applied to type members, telling Jackson to ignore the corresponding property when serializing or deserializing. The latter is used at any level, including type and type member, to list properties that should be ignored.

@JsonIgnoreProperties is more powerful than the other since it allows us to ignore properties inherited from supertypes that we do not have control of, such as types in an external library. In addition, this annotation allows us to ignore many properties at once, which can lead to more understandable code in some cases.

The following class structure is used to demonstrate annotation usage:

public abstract class Vehicle { private String make; private String model; protected Vehicle(String make, String model) { this.make = make; this.model = model; } // no-arg constructor, getters and setters } @JsonIgnoreProperties({ "model", "seatingCapacity" }) public abstract class Car extends Vehicle { private int seatingCapacity; @JsonIgnore private double topSpeed; protected Car(String make, String model, int seatingCapacity, double topSpeed) { super(make, model); this.seatingCapacity = seatingCapacity; this.topSpeed = topSpeed; } // no-arg constructor, getters and setters } public class Sedan extends Car { public Sedan(String make, String model, int seatingCapacity, double topSpeed) { super(make, model, seatingCapacity, topSpeed); } // no-arg constructor } public class Crossover extends Car { private double towingCapacity; public Crossover(String make, String model, int seatingCapacity, double topSpeed, double towingCapacity) { super(make, model, seatingCapacity, topSpeed); this.towingCapacity = towingCapacity; } // no-arg constructor, getters and setters }

As you can see, @JsonIgnore tells Jackson to ignore Car.topSpeed property, while @JsonIgnoreProperties ignores the Vehicle.model and Car.seatingCapacity ones.

The behavior of both annotations is validated by the following test. First, we need to instantiate ObjectMapper and data classes, then use that ObjectMapper instance to serialize data objects:

ObjectMapper mapper = new ObjectMapper(); Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0); Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0); List vehicles = new ArrayList(); vehicles.add(sedan); vehicles.add(crossover); String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString contains the following JSON array:

[ { "make": "Mercedes-Benz" }, { "make": "BMW", "towingCapacity": 6000.0 } ]

Finally, we will prove the presence or absence of various property names in the resulting JSON string:

assertThat(jsonDataString, containsString("make")); assertThat(jsonDataString, not(containsString("model"))); assertThat(jsonDataString, not(containsString("seatingCapacity"))); assertThat(jsonDataString, not(containsString("topSpeed"))); assertThat(jsonDataString, containsString("towingCapacity"));

3.2. Mix-ins

Mix-ins allow us to apply behavior (such as ignoring properties when serializing and deserializing) without the need to directly apply annotations to a class. This is especially useful when dealing with third-party classes, in which we cannot modify the code directly.

This sub-section reuses the class inheritance chain introduced in the previous one, except that the @JsonIgnore and @JsonIgnoreProperties annotations on the Car class have been removed:

public abstract class Car extends Vehicle { private int seatingCapacity; private double topSpeed; // fields, constructors, getters and setters }

In order to demonstrate operations of mix-ins, we will ignore Vehicle.make and Car.topSpeed properties, then use a test to make sure everything works as expected.

The first step is to declare a mix-in type:

private abstract class CarMixIn { @JsonIgnore public String make; @JsonIgnore public String topSpeed; }

Next, the mix-in is bound to a data class through an ObjectMapper object:

ObjectMapper mapper = new ObjectMapper(); mapper.addMixIn(Car.class, CarMixIn.class);

After that, we instantiate data objects and serialize them into a string:

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0); Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0); List vehicles = new ArrayList(); vehicles.add(sedan); vehicles.add(crossover); String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString now contains the following JSON:

[ { "model": "S500", "seatingCapacity": 5 }, { "model": "X6", "seatingCapacity": 5, "towingCapacity": 6000.0 } ]

Finally, let's verify the result:

assertThat(jsonDataString, not(containsString("make"))); assertThat(jsonDataString, containsString("model")); assertThat(jsonDataString, containsString("seatingCapacity")); assertThat(jsonDataString, not(containsString("topSpeed"))); assertThat(jsonDataString, containsString("towingCapacity"));

3.3. Annotation Introspection

Annotation introspection is the most powerful method to ignore supertype properties since it allows for detailed customization using the AnnotationIntrospector.hasIgnoreMarker API.

This sub-section makes use of the same class hierarchy as the preceding one. In this use case, we will ask Jackson to ignore Vehicle.model, Crossover.towingCapacity and all properties declared in the Car class. Let's start with the declaration of a class that extends the JacksonAnnotationIntrospector interface:

class IgnoranceIntrospector extends JacksonAnnotationIntrospector { public boolean hasIgnoreMarker(AnnotatedMember m)  }

The introspector will ignore any properties (that is, it will treat them as if they were marked as ignored via one of the other methods) that match the set of conditions defined in the method.

The next step is to register an instance of the IgnoranceIntrospector class with an ObjectMapper object:

ObjectMapper mapper = new ObjectMapper(); mapper.setAnnotationIntrospector(new IgnoranceIntrospector());

Now we create and serialize data objects in the same way as in section 3.2. The contents of the newly produced string are:

[ { "make": "Mercedes-Benz" }, { "make": "BMW" } ]

Finally, we'll verify that the introspector worked as intended:

assertThat(jsonDataString, containsString("make")); assertThat(jsonDataString, not(containsString("model"))); assertThat(jsonDataString, not(containsString("seatingCapacity"))); assertThat(jsonDataString, not(containsString("topSpeed"))); assertThat(jsonDataString, not(containsString("towingCapacity")));

4. Subtype Handling Scenarios

This section will deal with two interesting scenarios relevant to subclass handling.

4.1. Conversion Between Subtypes

Jackson allows an object to be converted to a type other than the original one. In fact, this conversion may happen among any compatible types, but it is most helpful when used between two subtypes of the same interface or class to secure values and functionality.

In order to demonstrate conversion of a type to another one, we will reuse the Vehicle hierarchy taken from section 2, with the addition of the @JsonIgnore annotation on properties in Car and Truck to avoid incompatibility.

public class Car extends Vehicle { @JsonIgnore private int seatingCapacity; @JsonIgnore private double topSpeed; // constructors, getters and setters } public class Truck extends Vehicle { @JsonIgnore private double payloadCapacity; // constructors, getters and setters }

The following code will verify that a conversion is successful and that the new object preserves data values from the old one:

ObjectMapper mapper = new ObjectMapper(); Car car = new Car("Mercedes-Benz", "S500", 5, 250.0); Truck truck = mapper.convertValue(car, Truck.class); assertEquals("Mercedes-Benz", truck.getMake()); assertEquals("S500", truck.getModel());

4.2. Deserialization Without No-arg Constructors

By default, Jackson recreates data objects by using no-arg constructors. This is inconvenient in some cases, such as when a class has non-default constructors and users have to write no-arg ones just to satisfy Jackson's requirements. It is even more troublesome in a class hierarchy where a no-arg constructor must be added to a class and all those higher in the inheritance chain. In these cases, creator methods come to the rescue.

This section will use an object structure similar to the one in section 2, with some changes to constructors. Specifically, all no-arg constructors are dropped, and constructors of concrete subtypes are annotated with @JsonCreator and @JsonProperty to make them creator methods.

public class Car extends Vehicle { @JsonCreator public Car( @JsonProperty("make") String make, @JsonProperty("model") String model, @JsonProperty("seating") int seatingCapacity, @JsonProperty("topSpeed") double topSpeed) { super(make, model); this.seatingCapacity = seatingCapacity; this.topSpeed = topSpeed; } // fields, getters and setters } public class Truck extends Vehicle { @JsonCreator public Truck( @JsonProperty("make") String make, @JsonProperty("model") String model, @JsonProperty("payload") double payloadCapacity) { super(make, model); this.payloadCapacity = payloadCapacity; } // fields, getters and setters }

Tes akan memverifikasi bahwa Jackson dapat menangani objek yang tidak memiliki konstruktor tanpa argumen:

ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); Car car = new Car("Mercedes-Benz", "S500", 5, 250.0); Truck truck = new Truck("Isuzu", "NQR", 7500.0); List vehicles = new ArrayList(); vehicles.add(car); vehicles.add(truck); Fleet serializedFleet = new Fleet(); serializedFleet.setVehicles(vehicles); String jsonDataString = mapper.writeValueAsString(serializedFleet); mapper.readValue(jsonDataString, Fleet.class);

5. Kesimpulan

Tutorial ini telah membahas beberapa kasus penggunaan yang menarik untuk mendemonstrasikan dukungan Jackson untuk pewarisan tipe, dengan fokus pada polimorfisme dan ketidaktahuan properti supertipe.

Penerapan semua contoh dan cuplikan kode ini dapat ditemukan di proyek GitHub.