Перейти к основному содержимому

Наследство с Джексоном

· 11 мин. чтения

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, и применим только к нескольким предварительно определенным типам типов.

Показанная выше структура Vehicle используется для заполнения экземпляра класса Fleet :

public class Fleet {
private List<Vehicle> vehicles;

// getters and setters
}

Чтобы внедрить метаданные типа, нам нужно включить функциональность типизации в объекте ObjectMapper , который позже будет использоваться для сериализации и десериализации объектов данных:

ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv, 
ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

Параметр PolymorphicTypeValidator используется для проверки того, что фактические подтипы для десериализации допустимы в соответствии с указанными критериями. Кроме того, параметр применимости определяет типы, для которых требуется информация о типе, а параметр includeAs представляет собой механизм включения метаданных типа. Кроме того, предоставляются два других варианта метода activDefaultTyping :

  • ObjectMapper. активироватьDefaultTyping(PolymorphicTypeValidator ptv, применимость ObjectMapper.DefaultTyping) : позволяет вызывающей стороне указать валидатор и применимость , используя WRAPPER_ARRAY в качестве значения по умолчанию для includeAs.
  • ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv): позволяет вызывающей стороне указать валидатор при использовании OBJECT_AND_NON_CONCRETE в качестве значения по умолчанию для применимости и WRAPPER_ARRAY в качестве значения по умолчанию для includeAs.

Давайте посмотрим, как это работает. Для начала нам нужно создать валидатор:

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.foreach.jackson.inheritance")
.allowIfSubType("java.util.ArrayList")
.build();

Далее давайте создадим объект ObjectMapper и активируем для него типизацию по умолчанию, используя приведенный выше валидатор:

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);

Следующим шагом является создание и заполнение структуры данных, представленной в начале этого подраздела. Код для этого будет повторно использован позже в последующих подразделах. Ради удобства и повторного использования мы назовем его блоком создания экземпляра транспортного средства .

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = new Truck("Isuzu", "NQR", 7500.0);

List<Vehicle> 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",
[
[
"com.foreach.jackson.inheritance.Car",
{
"make": "Mercedes-Benz",
"model": "S500",
"seatingCapacity": 5,
"topSpeed": 250.0
}
],

[
"com.foreach.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 и несколькими другими соответствующими аннотациями. В этом подразделе будет использоваться модель данных, аналогичная структуре Vehicle в предыдущем примере, чтобы проиллюстрировать аннотации для каждого класса. Единственное изменение — добавление аннотаций к абстрактному классу Vehicle , как показано ниже:

@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. Игнорирование свойств из супертипа

Иногда некоторые свойства, унаследованные от суперклассов, необходимо игнорировать во время сериализации или десериализации. Это может быть достигнуто одним из трех методов: аннотации, примеси и самоанализ аннотаций.

3.1. Аннотации

Есть две широко используемые аннотации Джексона для игнорирования свойств: @JsonIgnore и @JsonIgnoreProperties . Первый применяется непосредственно к членам типа, говоря Джексону игнорировать соответствующее свойство при сериализации или десериализации. Последний используется на любом уровне, включая тип и член типа, для перечисления свойств, которые следует игнорировать.

@JsonIgnoreProperties является более мощным, чем другие, поскольку позволяет игнорировать свойства, унаследованные от супертипов, над которыми мы не имеем контроля, например, типы во внешней библиотеке. Кроме того, эта аннотация позволяет игнорировать сразу множество свойств, что в некоторых случаях может привести к более понятному коду.

Следующая структура класса используется для демонстрации использования аннотаций:

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
}

Как видите, @JsonIgnore говорит Джексону игнорировать свойство Car.topSpeed , а @JsonIgnoreProperties игнорирует свойства Vehicle.model и Car.seatingCapacity .

Поведение обеих аннотаций проверяется следующим тестом. Во- первых, нам нужно создать экземпляры ObjectMapper и классов данных, а затем использовать этот экземпляр ObjectMapper для сериализации объектов данных:

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<Vehicle> vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString содержит следующий массив JSON:

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

Наконец, мы докажем наличие или отсутствие различных имен свойств в полученной строке JSON:

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. Примеси

Примеси позволяют нам применять поведение (например, игнорирование свойств при сериализации и десериализации) без необходимости напрямую применять аннотации к классу. Это особенно полезно при работе со сторонними классами, в которых мы не можем изменять код напрямую.

В этом подразделе повторно используется цепочка наследования классов, представленная в предыдущем, за исключением того, что аннотации @JsonIgnore и @JsonIgnoreProperties для класса Car были удалены:

public abstract class Car extends Vehicle {
private int seatingCapacity;
private double topSpeed;

// fields, constructors, getters and setters
}

Чтобы продемонстрировать работу дополнений, мы проигнорируем свойства Vehicle.make и Car.topSpeed , а затем воспользуемся тестом, чтобы убедиться, что все работает должным образом.

Первый шаг — объявить смешанный тип:

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

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

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

После этого мы создаем экземпляры объектов данных и сериализуем их в строку:

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0);
Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString теперь содержит следующий JSON:

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

Наконец, давайте проверим результат:

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

3.3. Аннотация

Интроспекция аннотаций — это самый мощный метод игнорирования свойств супертипа, поскольку он позволяет выполнять детальную настройку с помощью API AnnotationIntrospector.hasIgnoreMarker .

В этом подразделе используется та же иерархия классов, что и в предыдущем. В этом случае мы попросим Джексона игнорировать Vehicle.model , Crossover.towingCapacity и все свойства, объявленные в классе Car . Начнем с объявления класса, расширяющего интерфейс JacksonAnnotationIntrospector :

class IgnoranceIntrospector extends JacksonAnnotationIntrospector {
public boolean hasIgnoreMarker(AnnotatedMember m) {
return m.getDeclaringClass() == Vehicle.class && m.getName() == "model"
|| m.getDeclaringClass() == Car.class
|| m.getName() == "towingCapacity"
|| super.hasIgnoreMarker(m);
}
}

Интроспектор будет игнорировать любые свойства (то есть он будет рассматривать их так, как если бы они были помечены как игнорируемые с помощью одного из других методов), которые соответствуют набору условий, определенных в методе.

Следующим шагом является регистрация экземпляра класса IgnoranceIntrospector с объектом ObjectMapper :

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

Теперь мы создаем и сериализуем объекты данных так же, как в разделе 3.2. Содержимое вновь созданной строки:

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

Наконец, мы проверим, что интроспектор работает так, как задумано:

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. Сценарии обработки подтипов

В этом разделе будут рассмотрены два интересных сценария, относящихся к обработке подклассов.

4.1. Преобразование между подтипами

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

Чтобы продемонстрировать преобразование одного типа в другой, мы будем повторно использовать иерархию транспортных средств , взятую из раздела 2, с добавлением аннотации @JsonIgnore к свойствам в автомобилях и грузовиках , чтобы избежать несовместимости.

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
}

Следующий код проверяет, успешно ли выполнено преобразование и что новый объект сохраняет значения данных из старого:

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. Десериализация без конструкторов без аргументов

По умолчанию Джексон воссоздает объекты данных, используя конструкторы без аргументов. В некоторых случаях это неудобно, например, когда класс имеет конструкторы не по умолчанию, и пользователям приходится писать конструкторы без аргументов только для того, чтобы удовлетворить требования Джексона. Это еще более проблематично в иерархии классов, где конструктор без аргументов должен быть добавлен к классу и всем вышестоящим в цепочке наследования. В этих случаях на помощь приходят методы создания.

В этом разделе будет использоваться структура объекта, аналогичная структуре в разделе 2, с некоторыми изменениями в конструкторах. В частности, все конструкторы без аргументов удаляются, а конструкторы конкретных подтипов аннотируются @JsonCreator и @JsonProperty , чтобы сделать их методами-создателями.

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
}

Тест проверит, что Джексон может работать с объектами, у которых нет конструкторов без аргументов:

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<Vehicle> 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 .