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

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

1. обзор

В этой статье рассматриваются некоторые дополнительные аннотации, которые не были рассмотрены в предыдущей статье,A Guide to Jackson Annotations - мы рассмотрим семь из них.

2. @JsonIdentityReferenceс

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

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

Компонент без@JsonIdentityReference:

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

    // constructor, getters and setters
}

Для bean-компонента, использующего@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, вместо этого bean-компонент сериализуется как простой идентификатор:

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: все буквы в нижнем регистре без разделителей, например lowercase.

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

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

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

5. @JsonPropertyDescriptionс

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

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

В этом разделе используется объект bean, объявленный ниже, для демонстрации возможностей@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:example:jackson:annotation:extra:PropertyDescriptionBean",
    "properties":
    {
        "name":
        {
            "type": "string",
            "description": "This is a description of the name property"
        },

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

6. @JsonPOJOBuilderс

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

Предположим, нам нужно десериализовать следующую строку 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: имя метода без аргументов, используемого для создания экземпляра ожидаемого bean-компонента после привязки полей JSON к свойствам этого bean-компонента. Имя по умолчанию -build.

  • withPrefix: префикс имени для автоматического определения соответствия между JSON и свойствами bean-компонента. Префикс по умолчанию -with.

В этом примере используется класс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 для использования метода сборки под названиемcreateBean и префикса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с

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

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

Допустим, у нас есть следующее определение класса 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 при работе со следующей иерархией классов.

СуперклассAbstractBean:

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

ПодклассFirstBean:

public class FirstBean extends AbstractBean {
    String firstName;

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

    // no-arg constructor, getter and setter
}

ПодклассLastBean:

public class LastBean extends AbstractBean {
    String lastName;

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

    // no-arg constructor, getter and setter
}

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

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

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

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

Сначала нам нужно создать экземпляр контейнера 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);

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

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

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