Наследяване с Джаксън

1. Общ преглед

В тази статия ще разгледаме работата с йерархиите на класовете в Джаксън.

Два типични случая на използване са включването на метаданни от подтип и игнориране на свойствата, наследени от суперкласове. Ще опишем тези два сценария и няколко обстоятелства, при които е необходимо специално третиране на подвидовете.

2. Включване на информация за подтипа

Има два начина за добавяне на информация за типа при сериализиране и десериализиране на обекти на данни, а именно глобално въвеждане по подразбиране и анотации за клас.

2.1. Глобално въвеждане по подразбиране

Следващите три Java класа ще бъдат използвани за илюстриране на глобално включване на метаданни от тип.

Суперклас на превозното средство :

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 }

Подклас автомобили :

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 }

Подклас на камиона :

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 }

Глобалното въвеждане по подразбиране позволява информацията за типа да се декларира само веднъж, като я активира в обект ObjectMapper . Тогава метаданните от този тип ще бъдат приложени към всички определени типове. В резултат на това е много удобно да се използва този метод за добавяне на метаданни на типа, особено когато има голям брой типове. Недостатъкът е, че той използва напълно квалифицирани имена на типове Java като идентификатори на типа и по този начин е неподходящ за взаимодействия с не-Java системи и е приложим само за няколко предварително дефинирани видове типове.

На превозното средство структурата, показана по-горе се използва за попълване случай на Fleet клас:

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

За да вградим метаданни от тип, трябва да активираме функционалността за въвеждане на обекта ObjectMapper, която по-късно ще се използва за сериализация и десериализация на обекти от данни:

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

Параметърът за приложимост определя типовете, изискващи информация за типа, а параметър includeAs е механизмът за включване на метаданните на типа. Освен това се предоставят два други варианта на метода enableDefaultTyping :

  • ObjectMapper.enableDefaultTyping (ObjectMapper.DefaultTyping приложимост) : позволява на повикващия да посочи приложимостта , като същевременно използва WRAPPER_ARRAY като стойност по подразбиране за includeAs
  • ObjectMapper.enableDefaultTyping (): използва OBJECT_AND_NON_CONCRETE като стойност по подразбиране за приложимост и WRAPPER_ARRAY като стойност по подразбиране за includeAs

Нека да видим как работи. За начало трябва да създадем обект ObjectMapper и да активираме въвеждането по подразбиране върху него:

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

Полученият JSON низ:

{ "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 } ] ] ] }

По време на десериализацията обектите се възстановяват от JSON низ със запазени данни за тип:

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

Пресъздадените обекти ще бъдат същите конкретни подтипове, каквито са били преди сериализацията:

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

2.2. Анотации по клас

Анотацията по клас е мощен метод за включване на информация за типа и може да бъде много полезна за сложни случаи на употреба, при които е необходимо значително ниво на персонализиране. Това обаче може да бъде постигнато само за сметка на усложнението. Анотациите по клас отменят глобалното въвеждане по подразбиране, ако информацията за типа е конфигурирана и по двата начина.

За да се използва този метод, супертипът трябва да се коментира с @JsonTypeInfo и няколко други подходящи анотации. Този подраздел ще използва модел на данни, подобен на структурата на превозното средство в предишния пример, за да илюстрира анотации за всеки клас. Единствената промяна е добавянето на пояснения към абстрактния клас на превозното средство , както е показано по-долу:

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

Обектите с данни се създават с помощта на блока за инстанциране на превозното средство, въведен в предишния подраздел, и след това сериализирани:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

Сериализацията създава следната JSON структура:

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

Този низ се използва за пресъздаване на обекти с данни:

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

И накрая, целият напредък е потвърден:

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

3. Игнориране на свойствата от супертип

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 }

Тестът ще провери дали Джаксън може да се справя с обекти, които нямат конструктори no-arg:

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. Заключение

Този урок обхваща няколко интересни случая на употреба, за да демонстрира подкрепата на Джаксън за наследяване на тип, с акцент върху полиморфизма и непознаването на свойствата на супертипа.

Прилагането на всички тези примери и кодови фрагменти може да бъде намерено в проект на GitHub.