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

Использование необязательного с Джексоном

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

1. Введение

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

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

2. Обзор проблемы

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

2.1. Зависимость от Maven

Чтобы использовать Jackson, убедитесь, что мы используем его последнюю версию :

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.0</version>
</dependency>

2.2. Наш книжный объект

Затем создадим класс Book, содержащий одно обычное и одно необязательное поле:

public class Book {
String title;
Optional<String> subTitle;

// getters and setters omitted
}

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

2.3. Сериализация

Теперь давайте создадим экземпляр Book :

Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));

И, наконец, давайте попробуем сериализовать его с помощью Jackson ObjectMapper :

String result = mapper.writeValueAsString(book);

Мы увидим, что вывод необязательного поля содержит не его значение, а вместо этого вложенный объект JSON с полем с именем present :

{"title":"Oliver Twist","subTitle":{"present":true}}

Хотя это может показаться странным, на самом деле это то, чего мы должны ожидать.

В этом случае isPresent() является общедоступным получателем класса Optional . Это означает, что он будет сериализован со значением true или false , в зависимости от того, пуст он или нет. Это поведение сериализации Джексона по умолчанию.

Если мы подумаем об этом, мы хотим, чтобы фактическое значение поля субтитров было сериализовано.

2.4. Десериализация

Теперь давайте перевернем наш предыдущий пример, на этот раз пытаясь десериализовать объект в необязательный. Мы увидим, что теперь мы получаем JsonMappingException :

@Test(expected = JsonMappingException.class)
public void givenFieldWithValue_whenDeserializing_thenThrowException
String bookJson = "{ \"title\": \"Oliver Twist\", \"subTitle\": \"foo\" }";
Book result = mapper.readValue(bookJson, Book.class);
}

Посмотрим трассировку стека:

com.fasterxml.jackson.databind.JsonMappingException:
Can not construct instance of java.util.Optional:
no String-argument constructor/factory method to deserialize from String value ('The Parish Boy's Progress')

Такое поведение снова имеет смысл. По сути, Джексону нужен конструктор, который может принимать значение subtitle в качестве аргумента. Это не относится к нашему необязательному полю.

3. Решение

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

К счастью, эта проблема была решена для нас. У Джексона есть набор модулей, которые работают с типами данных JDK 8 , включая необязательный .

3.1. Зависимость и регистрация Maven

Во-первых, давайте добавим последнюю версию в качестве зависимости Maven:

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>

Теперь все, что нам нужно сделать, это зарегистрировать модуль в нашем ObjectMapper :

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());

3.2. Сериализация

Теперь давайте проверим это. Если мы попытаемся снова сериализовать наш объект Book , мы увидим, что теперь есть подзаголовок, а не вложенный JSON:

Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));
String serializedBook = mapper.writeValueAsString(book);

assertThat(from(serializedBook).getString("subTitle"))
.isEqualTo("The Parish Boy's Progress");

Если мы попытаемся сериализовать пустую книгу, она будет сохранена как null :

book.setSubTitle(Optional.empty());
String serializedBook = mapper.writeValueAsString(book);

assertThat(from(serializedBook).getString("subTitle")).isNull();

3.3. Десериализация

Теперь давайте повторим наши тесты на десериализацию. Если мы перечитаем нашу Книгу, то увидим, что больше не получаем JsonMappingException:

Book newBook = mapper.readValue(result, Book.class);

assertThat(newBook.getSubTitle()).isEqualTo(Optional.of("The Parish Boy's Progress"));

Наконец, давайте повторим тест еще раз, на этот раз с нулевым значением. Мы снова увидим, что мы не получаем JsonMappingException и фактически имеем пустой необязательный параметр:

assertThat(newBook.getSubTitle()).isEqualTo(Optional.empty());

4. Вывод

Мы показали, как обойти эту проблему, используя модуль JDK 8 DataTypes, продемонстрировав, как он позволяет Джексону обрабатывать пустой необязательный параметр как null, а существующий необязательный элемент — как обычное поле.

Реализацию этих примеров можно найти на GitHub ; это проект на основе Maven, поэтому его легко запустить как есть.