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

1. Обзор

В этой статье мы рассмотрим работу с иерархиями классов в Джексоне.

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

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

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

2.1. Глобальный ввод по умолчанию

Следующие три класса Java будут использованы для иллюстрации глобального включения метаданных типа.

Vehicle суперкласс:

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
}

Car подкласс:

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
}

Truck подкласс:

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.enableDefaultTyping(ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

Параметр applicability определяет типы, для которых требуется информация о типе, а параметр includeAs - это механизм для включения метаданных типа. Кроме того, предоставляются два других варианта метода enableDefaultTyping :

  • __ObjectMapper.

применимость) : позволяет вызывающей стороне указать applicability , в то время как используя WRAPPER ARRAY в качестве значения по умолчанию для includeAs ** ObjectMapper.enableDefaultTyping (): использует OBJECT AND NON CONCRETE__

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

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

В этом подразделе повторно используется цепочка наследования классов, представленная в предыдущем, за исключением того, что аннотации @ 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. Преобразование между подтипами

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

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

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

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

Реализация всех этих примеров и фрагментов кода может быть найдена в a проект GitHub .