Рубрики
Без рубрики

Больше аннотаций Джексона

В этой статье рассматриваются некоторые менее известные аннотации обработки JSON, предоставленные Джексоном.

Автор оригинала: baeldung.

1. Обзор

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

2. @JsonIdentityReference

@JsonIdentityReference используется для настройки ссылок на объекты, которые будут сериализованы как идентификаторы объектов вместо полных POJOS. Он работает в сотрудничестве с @JsonIdentityInfo , чтобы принудительно использовать идентификаторы объектов в каждой сериализации, отличной от всех, кроме первого, когда @JsonIdentityReference отсутствует. Эта пара аннотаций наиболее полезна при работе с циклическими зависимостями между объектами. Пожалуйста, обратитесь к разделу 4 статьи Джексон – Двунаправленные отношения для получения дополнительной информации.

Чтобы продемонстрировать использование @JsonIdentityReference , мы определим два разных класса бобов, без и с этой аннотацией.

Боб без @JsonIdentityReference :

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class BeanWithoutIdentityReference {
    private int id;
    private String name;

    // constructor, getters and setters
}

Для компонента , использующего @JsonIdentityReference , мы выбираем свойство id в качестве идентификатора объекта:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@JsonIdentityReference(alwaysAsId = true)
public class BeanWithIdentityReference {
    private int id;
    private String name;
    
    // constructor, getters and setters
}

В первом случае, когда @JsonIdentityReference отсутствует, этот компонент сериализуется с полной информацией о его свойствах:

BeanWithoutIdentityReference bean 
  = new BeanWithoutIdentityReference(1, "Bean Without Identity Reference Annotation");
String jsonString = mapper.writeValueAsString(bean);

Результат приведенной выше сериализации:

{
    "id": 1,
    "name": "Bean Without Identity Reference Annotation"
}

Когда используется @JsonIdentityReference , вместо этого компонент сериализуется как простое удостоверение:

BeanWithIdentityReference bean 
  = new BeanWithIdentityReference(1, "Bean With Identity Reference Annotation");
String jsonString = mapper.writeValueAsString(bean);
assertEquals("1", jsonString);

3. @JsonAppend

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

Предположим, что у нас есть боб без @JsonAppend следующим образом:

public class BeanWithoutAppend {
    private int id;
    private String name;

    // constructor, getters and setters
}

Тест подтвердит, что в отсутствие аннотации @JsonAppend выходные данные сериализации не содержат информации о дополнительном свойстве version , несмотря на то, что мы пытаемся добавить в объект ObjectWriter :

BeanWithoutAppend bean = new BeanWithoutAppend(2, "Bean Without Append Annotation");
ObjectWriter writer 
  = mapper.writerFor(BeanWithoutAppend.class).withAttribute("version", "1.0");
String jsonString = writer.writeValueAsString(bean);

Вывод сериализации:

{
    "id": 2,
    "name": "Bean Without Append Annotation"
}

Теперь предположим, что у нас есть боб с аннотацией @JsonAppend :

@JsonAppend(attrs = { 
  @JsonAppend.Attr(value = "version") 
})
public class BeanWithAppend {
    private int id;
    private String name;

    // constructor, getters and setters
}

Аналогичный предыдущему тест проверит, что при применении аннотации @JsonAppend дополнительное свойство включается после сериализации:

BeanWithAppend bean = new BeanWithAppend(2, "Bean With Append Annotation");
ObjectWriter writer 
  = mapper.writerFor(BeanWithAppend.class).withAttribute("version", "1.0");
String jsonString = writer.writeValueAsString(bean);

Результат этой сериализации показывает, что свойство version было добавлено:

{
    "id": 2,
    "name": "Bean With Append Annotation",
    "version": "1.0"
}

4. @JsonNaming

Аннотация @JsonNaming используется для выбора стратегий именования свойств в сериализации, переопределяя значение по умолчанию. Используя элемент value , мы можем указать любую стратегию, в том числе пользовательскую.

В дополнение к умолчанию, которое является LOWER_CAMEL_CASE (например, lowerCamelCase ), библиотека Джексона предоставляет нам четыре других встроенных стратегии именования свойств для удобства:

  • KEBAB_CASE : Элементы имени разделяются дефисами, например kebab-case .
  • LOWER_CASE : Все буквы строчные без разделителей, например строчные .
  • SNAKE_CASE : Все буквы строчные с подчеркиванием в качестве разделителей между элементами имени, например snake_case .
  • UPPER_CAMEL_CASE : Все элементы имени, включая первый, начинаются с заглавной буквы, за которой следуют строчные буквы, и нет разделителей, например UpperCamelCase .

Этот пример проиллюстрирует способ сериализации свойств с использованием имен прецедентов змеи, где свойство с именем beanName сериализуется как bean_name.

Дано определение боба:

@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class NamingBean {
    private int id;
    private String beanName;

    // constructor, getters and setters
}

Приведенный ниже тест показывает, что указанное правило именования работает должным образом:

NamingBean bean = new NamingBean(3, "Naming Bean");
String jsonString = mapper.writeValueAsString(bean);        
assertThat(jsonString, containsString("bean_name"));

Переменная json String содержит следующие данные:

{
    "id": 3,
    "bean_name": "Naming Bean"
}

5. @Jobpropertydescriptor

Библиотека Джексона способна создавать схемы JSON для типов Java с помощью отдельного модуля под названием JSON Schema . Схема полезна, когда мы хотим указать ожидаемый результат при сериализации объектов Java или проверить документ JSON перед десериализацией.

Аннотация @Jobpropertydescriptor позволяет добавить удобочитаемое описание в созданную схему JSON, указав поле description .

В этом разделе используется компонент, объявленный ниже, чтобы продемонстрировать возможности @JsonPropertyDescription :

public class PropertyDescriptionBean {
    private int id;
    @JsonPropertyDescription("This is a description of the name property")
    private String name;

    // getters and setters
}

Метод создания схемы JSON с добавлением поля description показан ниже:

SchemaFactoryWrapper wrapper = new SchemaFactoryWrapper();
mapper.acceptJsonFormatVisitor(PropertyDescriptionBean.class, wrapper);
JsonSchema jsonSchema = wrapper.finalSchema();
String jsonString = mapper.writeValueAsString(jsonSchema);
assertThat(jsonString, containsString("This is a description of the name property"));

Как мы видим, генерация схемы JSON прошла успешно:

{
    "type": "object",
    "id": "urn:jsonschema:com:baeldung:jackson:annotation:extra:PropertyDescriptionBean",
    "properties": 
    {
        "name": 
        {
            "type": "string",
            "description": "This is a description of the name property"
        },

        "id": 
        {
            "type": "integer"
        }
    }
}

6. @JsonPOJOBuilder

Аннотация @JsonPOJOBuilder используется для настройки класса построителя для настройки десериализации документа JSON для восстановления POJOS, когда соглашение об именовании отличается от соглашения по умолчанию.

Предположим, нам нужно десериализовать следующую строку JSON:

{
    "id": 5,
    "name": "POJO Builder Bean"
}

Этот источник JSON будет использоваться для создания экземпляра POJOBuilderBean :

@JsonDeserialize(builder = BeanBuilder.class)
public class POJOBuilderBean {
    private int identity;
    private String beanName;

    // constructor, getters and setters
}

Имена свойств компонента отличаются от имен полей в строке JSON. Именно здесь на помощь приходит @JsonPOJOBuilder .

Аннотация @JsonPOJOBuilder сопровождается двумя свойствами:

  • buildMethodName : Имя метода no-arg, используемого для создания экземпляра ожидаемого компонента после привязки полей JSON к свойствам этого компонента. Имя по умолчанию – build .
  • с префиксом : Префикс имени для автоматического определения соответствия между свойствами JSON и bean. Префикс по умолчанию – с .

В этом примере используется класс BeanBuilder ниже, который используется в POJOBuilderBean :

@JsonPOJOBuilder(buildMethodName = "createBean", withPrefix = "construct")
public class BeanBuilder {
    private int idValue;
    private String nameValue;

    public BeanBuilder constructId(int id) {
        idValue = id;
        return this;
    }

    public BeanBuilder constructName(String name) {
        nameValue = name;
        return this;
    }

    public POJOBuilderBean createBean() {
        return new POJOBuilderBean(idValue, nameValue);
    }
}

В приведенном выше коде мы настроили @JsonPOJOBuilder для использования метода сборки с именем create Bean и префикса construct для сопоставления свойств.

Применение @JsonPOJOBuilder к компоненту описывается и тестируется следующим образом:

String jsonString = "{\"id\":5,\"name\":\"POJO Builder Bean\"}";
POJOBuilderBean bean = mapper.readValue(jsonString, POJOBuilderBean.class);

assertEquals(5, bean.getIdentity());
assertEquals("POJO Builder Bean", bean.getBeanName());

Результат показывает, что новый объект данных был успешно воссоздан из источника JSON, несмотря на несоответствие имен свойств.

7. @JsonTypeId

Аннотация @Jsontypeinfo используется для указания на то, что аннотированное свойство должно быть сериализовано как идентификатор типа при включении информации о полиморфном типе, а не как обычное свойство. Эти полиморфные метаданные используются во время десериализации для воссоздания объектов тех же подтипов, что и до сериализации, а не объявленных супертипов.

Дополнительные сведения об обработке Джексоном наследования см. в разделе 2 Наследования в Джексоне .

Допустим, у нас есть определение класса bean следующим образом:

public class TypeIdBean {
    private int id;
    @JsonTypeId
    private String name;

    // constructor, getters and setters
}

Следующий тест подтверждает, что @JsonTypeId работает так, как задумано:

mapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
TypeIdBean bean = new TypeIdBean(6, "Type Id Bean");
String jsonString = mapper.writeValueAsString(bean);
        
assertThat(jsonString, containsString("Type Id Bean"));

Выходные данные процесса сериализации:

[
    "Type Id Bean",
    {
        "id": 6
    }
]

8. @JsonTypeIdResolver

Аннотация @JsonTypeIdResolver используется для обозначения обработчика идентификаторов пользовательских типов при сериализации и десериализации. Этот обработчик отвечает за преобразование между типами Java и идентификатором типа, включенным в документ JSON.

Предположим, что мы хотим встроить информацию о типе в строку JSON при работе со следующей иерархией классов.

Абстрактный суперкласс:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME, 
  include = JsonTypeInfo.As.PROPERTY, 
  property = "@type"
)
@JsonTypeIdResolver(BeanIdResolver.class)
public class AbstractBean {
    private int id;

    protected AbstractBean(int id) {
        this.id = id;
    }

    // no-arg constructor, getter and setter
}

Подкласс First Bean :

public class FirstBean extends AbstractBean {
    String firstName;

    public FirstBean(int id, String name) {
        super(id);
        setFirstName(name);
    }

    // no-arg constructor, getter and setter
}

Подкласс Last Bean :

public class LastBean extends AbstractBean {
    String lastName;

    public LastBean(int id, String name) {
        super(id);
        setLastName(name);
    }

    // no-arg constructor, getter and setter
}

Экземпляры этих классов используются для заполнения контейнера Bean объекта:

public class BeanContainer {
    private List beans;

    // getter and setter
}

Мы видим , что класс AbstractBean аннотирован с помощью @JsonTypeIdResolver , указывая, что он использует пользовательский TypeIdResolver , чтобы решить, как включить информацию о подтипе в сериализацию и как использовать эти метаданные наоборот.

Вот класс решателя для обработки включения информации о типе:

public class BeanIdResolver extends TypeIdResolverBase {
    
    private JavaType superType;

    @Override
    public void init(JavaType baseType) {
        superType = baseType;
    }

    @Override
    public Id getMechanism() {
        return Id.NAME;
    }

    @Override
    public String idFromValue(Object obj) {
        return idFromValueAndType(obj, obj.getClass());
    }

    @Override
    public String idFromValueAndType(Object obj, Class subType) {
        String typeId = null;
        switch (subType.getSimpleName()) {
        case "FirstBean":
            typeId = "bean1";
            break;
        case "LastBean":
            typeId = "bean2";
        }
        return typeId;
    }

    @Override
    public JavaType typeFromId(DatabindContext context, String id) {
        Class subType = null;
        switch (id) {
        case "bean1":
            subType = FirstBean.class;
            break;
        case "bean2":
            subType = LastBean.class;
        }
        return context.constructSpecializedType(superType, subType);
    }
}

Двумя наиболее заметными методами являются id Из значения и типа и type Из Id , причем первый указывает способ включения информации о типе при сериализации POJOS, а второй определяет подтипы воссозданных объектов с использованием этих метаданных.

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

Сначала нам нужно создать экземпляр контейнера bean и классы bean, а затем заполнить этот контейнер экземплярами bean:

FirstBean bean1 = new FirstBean(1, "Bean 1");
LastBean bean2 = new LastBean(2, "Bean 2");

List beans = new ArrayList<>();
beans.add(bean1);
beans.add(bean2);

BeanContainer serializedContainer = new BeanContainer();
serializedContainer.setBeans(beans);

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

String jsonString = mapper.writeValueAsString(serializedContainer);
assertThat(jsonString, containsString("bean1"));
assertThat(jsonString, containsString("bean2"));

Результат сериализации показан ниже:

{
    "beans": 
    [
        {
            "@type": "bean1",
            "id": 1,
            "firstName": "Bean 1"
        },

        {
            "@type": "bean2",
            "id": 2,
            "lastName": "Bean 2"
        }
    ]
}

Эта структура JSON будет использоваться для повторного создания объектов тех же подтипов, что и до сериализации. Вот шаги по реализации десериализации:

BeanContainer deserializedContainer = mapper.readValue(jsonString, BeanContainer.class);
List beanList = deserializedContainer.getBeans();
assertThat(beanList.get(0), instanceOf(FirstBean.class));
assertThat(beanList.get(1), instanceOf(LastBean.class));

9. Заключение

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