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

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

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

Упражнение: Сложение двух чисел

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

ANDROMEDA

1. Обзор

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

2. @JsonIdentityReference

@JsonIdentityReference используется для настройки ссылок на объекты, которые будут сериализованы как идентификаторы объектов вместо полных POJO. Он работает в сотрудничестве с @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
}

Для 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, а не изменять определение класса. Например, может быть удобнее вставить метаданные версии компонента в соответствующий документ JSON, чем предоставлять ему дополнительное свойство.

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

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

// constructor, getters and setters
}

Тест подтвердит, что при отсутствии аннотации @JsonAppend вывод сериализации не содержит информации о дополнительном свойстве версии , несмотря на то, что мы пытаемся добавить к объекту 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"
}

Теперь предположим, что у нас есть bean-компонент с аннотацией @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);

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

{
"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 : все буквы в нижнем регистре с символами подчеркивания в качестве разделителей между элементами имени, например, змея_case .
  • UPPER_CAMEL_CASE : Все элементы имени, включая первый, начинаются с заглавной буквы, за которой следуют строчные, и нет разделителей, например, UpperCamelCase .

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

Учитывая определение bean:

@JsonNaming(PropertyNamingStrategies.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, предоставив поле описания .

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

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

// getters and setters
}

Способ генерации схемы JSON с добавлением поля описания показан ниже:

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:foreach: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 : имя метода без аргументов, используемого для создания экземпляра ожидаемого компонента после привязки полей JSON к свойствам этого компонента. Имя по умолчанию — сборка .
  • withPrefix : префикс имени для автоматического определения соответствия между 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 для использования метода сборки, называемого createBean, и префикса конструкции для сопоставления свойств.

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

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 используется для указания того, что аннотированное свойство должно быть сериализовано как идентификатор типа при включении информации о полиморфном типе, а не как обычное свойство. Эти полиморфные метаданные используются во время десериализации для воссоздания объектов тех же подтипов, какими они были до сериализации, а не объявленных супертипов.

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

Суперкласс 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<AbstractBean> 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, а второй определяет подтипы воссозданных объектов с использованием этих метаданных.

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

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

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

List<AbstractBean> 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<AbstractBean> beanList = deserializedContainer.getBeans();
assertThat(beanList.get(0), instanceOf(FirstBean.class));
assertThat(beanList.get(1), instanceOf(LastBean.class));

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

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