Парсинг JSON с помощью Jackson

Большая часть веба на сегодняшний день обменивается данными в формате JSON. Веб-серверы, веб-приложения и мобильные приложения, даже устройства IoT общаются друг с другом, используя JSON. Простой и гибкий способ обработки JSON необходим любому программному обеспечению, чтобы выжить в современном мире.
Эта статья сопровождается примером рабочего кода на GitHub.
Что такое JSON?
JSON (от англ JavaScript Object Notation) — это текстовый формат для представления структурированных данных на основе синтаксиса объектов JavaScript. Благодаря своему гибкому и простому формату он стал чрезвычайно популярным. По сути, он следует модели карты «ключ-значение», допускающей вложенные объекты и массивы:
< "array": [ 1, 2, 3 ], "boolean": true, "color": "gold", "null": null, "number": 123, "object": < "a": "b", "c": "d" >, "string": "Hello World" >
Что такое Jackson?
Jackson в основном известен как библиотека, которая конвертирует строки JSON и простые объекты Java (англ POJO — Plain Old Java Object). Он также поддерживает многие другие форматы данных, такие как CSV, YML и XML.
Многие предпочитают Jackson благодаря его зрелости (он существует уже 13 лет) и отличной интеграции с популярными фреймворками, такими как Spring. Более того, это проект с открытым исходным кодом, который активно развивается и поддерживается широким сообществом.
Под капотом у Jackson есть три основных пакета: Streaming, Databind и Annotations. При этом Jackson предлагает нам три способа обработки преобразования JSON-POJO:
Потоковое API
Это самый быстрый подход из трех и с наименьшими накладными расходами. Он читает и записывает содержимое JSON в виде дискретных событий. API предоставляет JsonParser, который считывает JSON в POJO, и JsonGenerator, который записывает POJO в JSON.
Модель дерева
Модель дерева создает в памяти древовидное представление документа JSON. ObjectMapper отвечает за построение дерева из узлов JsonNode. Это наиболее гибкий подход, поскольку он позволяет перемещаться по дереву узлов, когда документ JSON не соответствует в достаточной мере POJO.
Привязка данных
Это позволяет нам выполнять преобразование между документами POJO и JSON с помощью средств доступа к свойствам или с помощью аннотаций. Он предлагает два типа привязки:
- Простая привязка данных, которая преобразует JSON в Java Maps, Lists, Strings, Numbers, Booleans, null объекты и обратно.
- Полная привязка данных, которая преобразует JSON в любой класс Java и из него.
ObjectMapper
ObjectMapper — наиболее часто используемая часть библиотеки Jackson, так как является самым простым способом преобразования между POJO и JSON. Она находится в com.fasterxml.jackson.databind .
Метод readValue() используется для преобразования (десериализации) JSON из строки, потока или файла в POJO.
С другой стороны, метод writeValue() используется для преобразования POJO в JSON (сериализация).
Способ, которым ObjectMapper определяет, какое поле JSON соответствует какому полю POJO, заключается в сопоставлении имен полей JSON с именами геттеров и сеттеров в POJO.
Это делается путем удаления частей «get» и «set» в именах геттеров и сеттеров и преобразования первого символа имени оставшегося метода в нижний регистр.
Например, предположим, у нас есть поле JSON с именем name : ObjectMapper сопоставит его с геттером getName() и сеттером setName() в POJO.
ObjectMapper является настраиваемым, и мы можем кастомизировать его в соответствии с нашими потребностями либо непосредственно через экземпляр ObjectMapper , либо с помощью аннотаций Jackson, как мы увидим позже.
Зависимости Maven
Прежде чем мы посмотрим на код, нам нужно добавить зависимость Jackson Maven jackson-databind, которая, в свою очередь, транзитивно добавляет jackson-annotations и jackson-core .
com.fasterxml.jackson.core jackson-databind 2.13.3
Мы также используем Lombok для обработки шаблонного кода для геттеров, сеттеров и конструкторов.
Базовая сериализация и десериализация JSON с Jackson
Давайте рассмотрим наиболее важные варианты использования Jackson с примерами кода.
Базовое преобразование POJO/JSON с использованием ObjectMapper
Давайте начнем с представления простого POJO под названием Employee:
@Getter @AllArgsConstructor @NoArgsConstructor public class Employee
Начнем с преобразования POJO в строку JSON:
public class JacksonTest < ObjectMapper objectMapper = new ObjectMapper(); @Test void pojoToJsonString() throws JsonProcessingException < Employee employee = new Employee("Mark", "James", 20); String json = objectMapper.writeValueAsString(employee); System.out.println(json); >>
В качестве вывода увидим следующее:
Теперь посмотрим, как преобразовать строку JSON в объект Employee с помощью ObjectMapper .
public class JacksonTest < . @Test void jsonStringToPojo() throws JsonProcessingException < String employeeJson = ""; Employee employee = objectMapper.readValue(employeeJson, Employee.class); assertThat(employee.getFirstName()).isEqualTo("Jalil"); > >
ObjectMapper также предлагает богатый API для чтения JSON из разных источников в разные форматы, давайте проверим самые важные из них.
Создание POJO из файла JSON
Это делается с помощью метода readValue() .
Файл JSON в тестовых ресурсах employee.json :
public class JacksonTest < . @Test void jsonFileToPojo() throws IOException < File file = new File("src/test/resources/employee.json"); Employee employee = objectMapper.readValue(file, Employee.class); assertThat(employee.getAge()).isEqualTo(44); assertThat(employee.getLastName()).isEqualTo("Simpson"); assertThat(employee.getFirstName()).isEqualTo("Homer"); >>
Создание POJO из массива байт в формате JSON
public class JacksonTest < . @Test void byteArrayToPojo() throws IOException < String employeeJson = ""; Employee employee = objectMapper.readValue(employeeJson.getBytes(), Employee.class); assertThat(employee.getFirstName()).isEqualTo("Jalil"); > >
Создание списка POJO из JSON
Иногда документ JSON представляет собой не объект, а список объектов. Давайте посмотрим, как можно его прочитать.
public class JacksonTest < . @Test void fileToListOfPojos() throws IOException < File file = new File("src/test/resources/employeeList.json"); ListemployeeList = objectMapper.readValue(file, new TypeReference<>()<>); assertThat(employeeList).hasSize(2); assertThat(employeeList.get(0).getAge()).isEqualTo(33); assertThat(employeeList.get(0).getLastName()).isEqualTo("Simpson"); assertThat(employeeList.get(0).getFirstName()).isEqualTo("Marge"); > >
Создание Map из JSON
Мы можем преобразовать JSON в Java Map , что очень удобно, если мы не знаем, чего ожидать от файла JSON, который мы пытаемся спарсить. ObjectMapper превратит имя каждой переменной в JSON в ключ для Map, а значение этой переменной — в значение по этому ключу.
public class JacksonTest < . @Test void fileToMap() throws IOException < File file = new File("src/test/resources/employee.json"); Mapemployee = objectMapper.readValue(file, new TypeReference<>()<>); assertThat(employee.keySet()).containsExactly("firstName", "lastName", "age"); assertThat(employee.get("firstName")).isEqualTo("Homer"); assertThat(employee.get("lastName")).isEqualTo("Simpson"); assertThat(employee.get("age")).isEqualTo(44); > >
Игнорирование неизвестных полей JSON
Иногда ожидаемый нами JSON может иметь дополнительные поля, не определенные в POJO. Поведение Jackson по умолчанию заключается в том, чтобы в таких случаях генерировать исключение UnrecognizedPropertyException . Однако же мы можем настроить Jackson так, чтобы он не расстраивался по поводу неизвестных полей и просто игнорировал их. Это делается путем установки FAIL_ON_UNKNOWN_PROPERTIES ObjectMapper в false.
public class JacksonTest < . @Test void fileToPojoWithUnknownProperties() throws IOException < File file = new File("src/test/resources/employeeWithUnknownProperties.json"); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Employee employee = objectMapper.readValue(file, Employee.class); assertThat(employee.getFirstName()).isEqualTo("Homer"); assertThat(employee.getLastName()).isEqualTo("Simpson"); assertThat(employee.getAge()).isEqualTo(44); >>
Работа с датами in Jackson
Преобразование дат может быть непростым занятием, поскольку они могут быть представлены во многих форматах и уровнях спецификации (секунды, миллисекунды и т. д.).
Дата в JSON
Прежде чем говорить о преобразовании дат и Jackson, нам нужно поговорить о новом Date API в Java 8. Он был введен для устранения недостатков более старых java.util.Date и java.util.Calendar . В основном нас интересует использование класса LocalDate , который предлагает эффективный способ представления даты и времени.
Для этого нам нужно добавить в Jackson дополнительный модуль, чтобы он мог обрабатывать LocalDate .
com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.13.3
Затем нам нужно сказать ObjectMapper найти и зарегистрировать новый модуль, который мы только что добавили.
public class JacksonTest < ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); . @Test void orderToJson() throws JsonProcessingException < Order order = new Order(1, LocalDate.of(1900,2,1)); String json = objectMapper.writeValueAsString(order); System.out.println(json); >>
В этом случае поведение Jackson по умолчанию состоит в том, чтобы показывать дату как [гггг-ММ-дд]. Таким образом, вывод будет
Однако мы можем указать Jackson, в каком формате нам нужна дата. Это можно сделать с помощью аннотации @JsonFormat .
public class Order < private int id; @JsonFormat(pattern = "dd/MM/yyyy") private LocalDate date; >
@Test void orderToJsonWithDate() throws JsonProcessingException
JSON в дату
Мы можем использовать ту же конфигурацию выше, чтобы преобразовать поле JSON в дату.
public class JacksonTest < . @Test void fileToOrder() throws IOException < File file = new File("src/test/resources/order.json"); Order order = objectMapper.readValue(file, Order.class); assertThat(order.getDate().getYear()).isEqualTo(2000); assertThat(order.getDate().getMonthValue()).isEqualTo(4); assertThat(order.getDate().getDayOfMonth()).isEqualTo(30); >>
Аннотации Jackson
Важную роль в настройке процесса преобразования JSON/POJO играют аннотации. Мы видели пример с преобразованием даты, где мы использовали аннотацию @JsonFormat . Аннотации влияют на то, как данные читаются, записываются или даже на то и другое. Давайте рассмотрим некоторые из этих аннотаций на основе их категорий.
Аннотации чтения
Они влияют на то, как Jackson преобразует JSON в POJO.
@JsonSetter
Это полезно, когда мы хотим сопоставить поле в строке JSON с полем в POJO, где их имена не совпадают.
@NoArgsConstructor @AllArgsConstructor @Getter public class Car
public class JacksonTest < . @Test void fileToCar() throws IOException < File file = new File("src/test/resources/car.json"); Car car = objectMapper.readValue(file, Car.class); assertThat(car.getBrand()).isEqualTo("BMW"); >>
@JsonAnySetter
Эта аннотация полезна в случаях, когда JSON содержит некоторые поля, не объявленные в POJO. Он используется с сеттером, который вызывается для каждого нераспознанного поля.
public class Car < @JsonSetter("carBrand") private String brand; private MapunrecognizedFields = new HashMap<>(); @JsonAnySetter public void allSetter(String fieldName, String fieldValue) < unrecognizedFields.put(fieldName, fieldValue); >>
public class JacksonTest < . @Test void fileToUnrecognizedCar() throws IOException < File file = new File("src/test/resources/carUnrecognized.json"); Car car = objectMapper.readValue(file, Car.class); assertThat(car.getUnrecognizedFields()).containsKey("productionYear"); >>
Аннотации записи
Они влияют на то, как Jackson преобразует POJO в JSON.
@JsonGetter
Это полезно, когда мы хотим сопоставить поле POJO с полем JSON, используя другое имя. Например, предположим, что у нас есть класс Cat с полем name , но мы хотим, чтобы его JSON-имя было catName .
@NoArgsConstructor @AllArgsConstructor public class Cat < private String name; @JsonGetter("catName") public String getName() < return name; >>
public class JacksonTest < . @Test void catToJson() throws JsonProcessingException < Cat cat = new Cat("Monica"); String json = objectMapper.writeValueAsString(cat); System.out.println(json); >>
@JsonAnyGetter
Эта аннотация позволяет нам использовать объект Map как источник свойств JSON. Скажем, у нас есть эта карта как поле в классе Cat .
@NoArgsConstructor @AllArgsConstructor public class Cat < private String name; @JsonAnyGetter Mapmap = Map.of( "name", "Jack", "surname", "wolfskin" ); . >
@Test void catToJsonWithMap() throws JsonProcessingException
Вывод будет следующим:
Аннотации чтения и записи
Эти аннотации влияют как на чтение, так и на запись JSON.
@JsonIgnore
Поле с аннотацией игнорируется как при записи, так и при чтении JSON.
@AllArgsConstructor @NoArgsConstructor @Getter public class Dog
public class JacksonTest < . @Test void dogToJson() throws JsonProcessingException < Dog dog = new Dog("Max", 3); String json = objectMapper.writeValueAsString(dog); System.out.println(json); >>
То же самое относится и к чтению в POJO.
Предположим, у нас есть файл dog.json :
public class JacksonTest < . @Test void fileToDog() throws IOException < File file = new File("src/test/resources/dog.json"); Dog dog = objectMapper.readValue(file, Dog.class); assertThat(dog.getName()).isEqualTo("bobby"); assertThat(dog.getAge()).isNull(); >>
У Jackson есть еще много полезных аннотаций, которые дают нам больше контроля над процессом сериализации/десериализации. Полный их список можно найти в репозитории Jackson на Github.
Резюме
- Jackson — одна из самых мощных и популярных библиотек для обработки JSON в Java.
- Jackson включает три основных модуля: Streaming API, Tree Model и Data Binding.
- Jackson предоставляет ObjectMapper, который легко настраивается в соответствии с потребностями, с возможностью задавать его свойства и использовать аннотации.
Приглашаем всех желающих на открытый урок «Реляционные базы данных для начинающих Java-разработчиков». На уроке поговорим о месте реляционных баз данных в архитектуре информационных систем. Рассмотрим основные компоненты и возможности РСУБД на примере PostgreSQL. Сделаем обзор основных технологий по работе с реляционными БД в Java (JDBC, JPA/Hibernate, Spring Data и др.). Регистрируйтесь по ссылке.
- Блог компании OTUS
- Программирование
- Java
Не самые очевидные советы по написанию DTO на Java
Сегодня приложения зачастую имеют распределенный характер. Для подключения к другим сервисам нужно писать больше кода — и при этом стараться сделать его простым.
Чтобы воспользоваться данными из внешней службы, мы обычно преобразуем полезную нагрузку JSON в объект передачи данных (Data Transfer Object, DTO). Код, обрабатывающий DTO, быстро усложняется, но с этим могут помочь несколько советов. Вполне возможно писать DTO, с которыми легче взаимодействовать и которые облегчают написание и чтение кода. Если объединить их вместе — можно упростить себе работу.
Сериализация DTO “по учебнику”
Начнем с типичного способа работы с JSON. Вот структура JSON. Этот JSON представляет пиццу “Реджина”.
<
"name": "Regina",
"ingredients": ["Ham", "Mushrooms", "Mozzarella", "Tomato purée"]
>
Чтобы воспользоваться этими данными у себя в приложении, я создам простой DTO с именем PizzaDto .
import java.util.List;
public static class PizzaDto private String name;
private List ingredients;
public String getName() return name;
>
public void setName(String name) this.name = name;
>
public List getIngredients() return ingredients;
>
public void setIngredients(List ingredients) this.ingredients = ingredients;
>
>
PizzaDto — «старый добрый Java-объект», POJO: объект со свойствами, геттерами, сеттерами и всем остальным. Он отражает структуру JSON, поэтому преобразование между объектом и JSON занимает всего одну строку. Вот пример этого с библиотекой Jackson:
String json = """
<
"name": "Regina",
"ingredients": [ "Ham", "Mushrooms", "Mozzarella", "Tomato purée" ]
>
""";
// из JSON в объект
PizzaDto dto = new ObjectMapper().readValue(json, PizzaDto.class);
// из объекта JSON
json = new ObjectMapper().writeValueAsString(dto);
Преобразование простое и прямолинейное. В чем же тогда проблема?
В реальной жизни DTO бывают довольно сложными. Код для создания и инициализации DTO может включать вплоть до десятков строк. Иногда больше. Это проблема, потому что сложный код содержит больше ошибок и менее чувствителен к изменениям.
Моя первая попытка упростить создание DTO — воспользоваться неизменяемым DTO: таким, который нельзя модифицировать после создания.
Такой подход может показаться странным, если вы не знакомы с этой идеей, поэтому давайте сосредоточимся на ней поподробнее.
Создание неизменяемых DTO
Если говорить просто, то объект неизменяемый, если его состояние не может поменяться после сборки.
Давайте перепишем PizzaDto , чтобы сделать его неизменяемым.
import java.util.List;
public class PizzaDto
private final String name;
private final List ingredients;
public PizzaDto(String name, List ingredients) this.name = name;
if (ingredients != null) ingredients = List.copyOf(ingredients);
>
this.ingredients = ingredients;
>
public String getName() return name;
>
public List getIngredients() return ingredients;
>
>
У неизменяемого объекта нет сеттера. Все его свойства — окончательные и должны быть инициализированы при построении.
Как вы можете видеть, список ингредиентов не хранится как есть. Вместо этого для сохранения неизменяемой копии входных данных используется List.copyOf() . Это не позволяет клиентам изменять ингредиенты, хранящиеся в DTO.
dto
.getIngredients()
.remove("Mushrooms"); // вызывает UnsupportedOperationException
Это важно, потому что пицца “Реджина” без грибов — уже определенно не пицца “Реджина”.
Если серьезнее, то Джошуа Блох, автор книги “Java: эффективное программирование”, дает такую рекомендацию для создания неизменяемых классов:
“Если в вашем классе есть какие-либо поля, которые ссылаются на изменяемые объекты, убедитесь, что клиенты класса не могут получать ссылки на эти объекты”. — Джошуа Блох
Если какое-либо свойство вашего DTO является изменяемым, вам необходимо сделать защитные копии. С их помощью вы предотвратите модификацию вашего DTO извне.
Примечание: начиная с Java 16, существует более краткий способ создания неизменяемых классов через записи.
Хорошо. Теперь у нас есть неизменяемый DTO. Но как это упрощает код?
Преимущества неизменяемости
Неизменяемость приносит много преимуществ, но вот мое любимое: неизменяемые переменные не имеют побочных эффектов.
Рассмотрим на примере. В этом фрагменте кода есть ошибка:
var pizza = make();
verify(pizza);
serve(pizza);
После выполнения этого кода пицца не содержит ожидаемого состояния. Какая строка вызвала проблему?
Попробуем два ответа: сначала с изменяемой переменной, а затем с неизменяемой.
Первый ответ — с изменяемой пиццей. pizza создается с помощью make() , но ее можно изменить в рамках verify() и serve() . Таким образом, к ошибке может приводить любая строка из трех.
Теперь второй ответ — с неизменяемой пиццей. make() возвращает пиццу, но verify() и serve() не могут ее изменить. К проблеме может приводить только make() . Здесь гораздо меньше пространства для расследования. Ошибку легче найти.
С неизменяемыми переменными отладка становится проще. Но это еще не все.
Когда пицца не валидна, метод verify() , вероятно, создает исключение, чтобы прервать процесс. Изменим это. Нам нужно, чтобы метод verify() исправлял невалидные пиццы.
Поскольку pizza — неизменяемый объект, verify() не может просто исправить его. Придется создавать и возвращать измененную пиццу, а клиентский код необходимо адаптировать:
var pizza = make();
pizza = verify(pizza);
serve(pizza);
В этой новой версии очевидно, что метод verify() возвращает новую исправленную пиццу. Неизменяемость делает код более понятным. Его становится легче читать и легче развивать.
Возможно, вы не знаете, но мы и так каждый день пользуемся неизменяемыми объектами. java.lang.String , java.math.BigDecimal , java.io.File — все они неизменяемые.
Есть и другие преимущества В своей книге Джошуа Блох просто рекомендует “минимизировать изменчивость”.
“Неизменяемые классы проще проектировать, реализовывать и использовать, чем изменяемые классы. Они менее подвержены ошибкам и более безопасны”. — Джошуа Блох
Теперь возникает интересный вопрос: можем ли мы поступать так же с DTO?
Неизменяемые DTO… А это осмысленно?
Цель DTO — передача данных между процессами. Объект инициализируется, а затем его состояние не должно меняться. Либо он будет сериализован в JSON, либо будет использоваться клиентом. Это делает неизменность естественной. Неизменяемый DTO будет передавать данные между процессами с гарантией.
Тогда почему я сначала написал изменяемое PizzaDTO , а не неизменяемое? Дело в уверенности, что моей библиотеке JSON требуются геттеры и сеттеры для DTO.
Как оказалось, это не соответствует истине.
Неизменяемые DTO с Jackson
Jackson — самая распространенная JSON-библиотека для Java.
Когда у DTO есть геттеры и сеттеры, Jackson может сопоставить объект с JSON без какой-либо дополнительной настройки. Но с неизменяемыми объектами Jackson нуждается в небольшой помощи. Ему нужно знать, как собирать объект.
Конструктор объекта должен быть снабжен аннотацией @JsonCreator , а каждый аргумент — @JsonProperty . Добавим эти аннотации в конструктор DTO.
// новый импорт:
// import com.fasterxml.jackson.annotation.*;
@JsonCreator
public PizzaDto(
@JsonProperty("name") String name,
@JsonProperty("ingredients") List ingredients) this.name = name;
if (ingredients != null) ingredients = List.copyOf(ingredients);
>
this.ingredients = ingredients;
>
Вот и все. Теперь нас есть неизменяемый DTO, который Jackson может преобразовать в JSON и обратно в объект.
Неизменяемые DTO с Gson и Moshi
Есть две альтернативы Jackson: Gson и Moshi.
С помощью этих библиотек еще проще преобразовать JSON в неизменяемый DTO, потому что им не нужны никакие дополнительные аннотации.
Но почему Jackson вообще требует аннотаций, в отличие от Gson и Moshi?
Никакой магии. Дело в том, что, когда Gson и Moshi генерируют объект из JSON, они создают и инициализируют его путем отражения. Кроме того, они не задействуют конструкторы.
Я не большой поклонник такого подхода. Он вводит в заблуждение, потому что разработчик может вложить некоторую логику в конструктор и никогда не узнать, что он не вызывается. По сравнению с этим, Jackson представляется гораздо более безопасным.
Избегайте нулевых значений
У Jackson есть еще одно преимущество. Если поместить в конструктор некоторую логику, он будет вызываться всегда, независимо от того, создан ли DTO кодом приложения или сгенерирован из JSON.
Можно воспользоваться этим преимуществом для избегания значений null и улучшить конструктор для инициализации полей с ненулевыми значениями.
В приведенном ниже фрагменте кода поля инициализируются пустыми значениями, когда входные данные равны нулю.
// новый импорт :
// import static org.apache.commons.lang3.ObjectUtils.firstNonNull;
@JsonCreator
public PizzaDto(
@JsonProperty("name") String name,
@JsonProperty("ingredients") List ingredients) this.name = firstNonNull(name, ""); // replace null by empty String
this.ingredients = List.copyOf(
firstNonNull(ingredients, List.of()) // replace null by empty List
);
>
В большинстве случаев пустые значения и null не отличаются в поведении. Если заменить нулевые значения пустыми, клиенты смогут пользоваться свойствами DTO без предварительной проверки на null -значения. Кроме того, это снижает шанс появления NullPointerException.
Так вы напишете меньше кода и повысите надежность. Что может быть лучше?
И последнее по счету, но не по важности: создавайте DTO со строителями
Есть еще один совет, как упростить инициализацию DTO. В комплекте с каждым DTO я создаю Builder. Он предоставляет свободный API для облегчения инициализации DTO.
Вот пример создания PizzaDto через сборщик:
var pizza = new PizzaDto.Builder()
.name("Regina")
.ingredients("Mozzarella cheese", "Basil leaves", "Olive oil", "Tomato purée")
.build();
С помощью сложных DTO разработчики делают код более выразительным. Этот шаблон настолько великолепен, что Джошуа Блох почти начинает с него свою книгу “Java: эффективное программирование”.
“Такой клиентский код легко писать и, что более важно, читать”. — Джошуа Блох
Как это работает? Объект builder просто хранит значения, пока мы не вызовем build() , который фактически создает нужный объект с сохраненными значениями.
Вот пример для PizzaDto :
public static final class Builder
private String name;
private List ingredients;
public Builder name(String name) this.name = name;
return this;
>
public Builder ingredients(List ingredients) this.ingredients = ingredients;
return this;
>
/**
* перегружает чтобы тот принимал String varargs
*/
public Builder ingredients(String. ingredients) <
return ingredients(List.of(ingredients));
>
public PizzaDto build() return new PizzaDto(name, ingredients);
>
>
Некоторые пользуются Lombok для создания конструкторов во время компиляции. Это упрощает DTO.
Я предпочитаю генерировать код конструктора с помощью плагина Builder generator IntelliJ. Затем можно добавить перегрузки методов, как в предыдущем фрагменте кода. Конструктор таким образом становится более гибким, а клиентский код — более компактным.
Заключение
Вот основные советы, которые я держу в голове при написании DTO. Соединенные вместе, они действительно улучшат ваш код. Кодовая база становится легче для чтения, проще в обслуживании и, в конечном счете, так проще делиться ею с вашей командой.
- Пишем асинхронный неблокирующий Rest API на Java
- Состояния потоков в Java
- Основы программирования UDP-сокетов на Java
Парсинг JSON с помощью Jackson

Большая часть веба на сегодняшний день обменивается данными в формате JSON. Веб-серверы, веб-приложения и мобильные приложения, даже устройства IoT общаются друг с другом, используя JSON. Простой и гибкий способ обработки JSON необходим любому программному обеспечению, чтобы выжить в современном мире.
Эта статья сопровождается примером рабочего кода на GitHub.
Что такое JSON?
JSON (от англ JavaScript Object Notation) — это текстовый формат для представления структурированных данных на основе синтаксиса объектов JavaScript. Благодаря своему гибкому и простому формату он стал чрезвычайно популярным. По сути, он следует модели карты «ключ-значение», допускающей вложенные объекты и массивы:
< "array": [ 1, 2, 3 ], "boolean": true, "color": "gold", "null": null, "number": 123, "object": < "a": "b", "c": "d" >, "string": "Hello World" >
Что такое Jackson?
Jackson в основном известен как библиотека, которая конвертирует строки JSON и простые объекты Java (англ POJO — Plain Old Java Object). Он также поддерживает многие другие форматы данных, такие как CSV, YML и XML.
Многие предпочитают Jackson благодаря его зрелости (он существует уже 13 лет) и отличной интеграции с популярными фреймворками, такими как Spring. Более того, это проект с открытым исходным кодом, который активно развивается и поддерживается широким сообществом.
Под капотом у Jackson есть три основных пакета: Streaming, Databind и Annotations. При этом Jackson предлагает нам три способа обработки преобразования JSON-POJO:
Потоковое API
Это самый быстрый подход из трех и с наименьшими накладными расходами. Он читает и записывает содержимое JSON в виде дискретных событий. API предоставляет JsonParser, который считывает JSON в POJO, и JsonGenerator, который записывает POJO в JSON.
Модель дерева
Модель дерева создает в памяти древовидное представление документа JSON. ObjectMapper отвечает за построение дерева из узлов JsonNode. Это наиболее гибкий подход, поскольку он позволяет перемещаться по дереву узлов, когда документ JSON не соответствует в достаточной мере POJO.
Привязка данных
Это позволяет нам выполнять преобразование между документами POJO и JSON с помощью средств доступа к свойствам или с помощью аннотаций. Он предлагает два типа привязки:
- Простая привязка данных, которая преобразует JSON в Java Maps, Lists, Strings, Numbers, Booleans, null объекты и обратно.
- Полная привязка данных, которая преобразует JSON в любой класс Java и из него.
ObjectMapper
ObjectMapper — наиболее часто используемая часть библиотеки Jackson, так как является самым простым способом преобразования между POJO и JSON. Она находится в com.fasterxml.jackson.databind .
Метод readValue() используется для преобразования (десериализации) JSON из строки, потока или файла в POJO.
С другой стороны, метод writeValue() используется для преобразования POJO в JSON (сериализация).
Способ, которым ObjectMapper определяет, какое поле JSON соответствует какому полю POJO, заключается в сопоставлении имен полей JSON с именами геттеров и сеттеров в POJO.
Это делается путем удаления частей «get» и «set» в именах геттеров и сеттеров и преобразования первого символа имени оставшегося метода в нижний регистр.
Например, предположим, у нас есть поле JSON с именем name : ObjectMapper сопоставит его с геттером getName() и сеттером setName() в POJO.
ObjectMapper является настраиваемым, и мы можем кастомизировать его в соответствии с нашими потребностями либо непосредственно через экземпляр ObjectMapper , либо с помощью аннотаций Jackson, как мы увидим позже.
Зависимости Maven
Прежде чем мы посмотрим на код, нам нужно добавить зависимость Jackson Maven jackson-databind, которая, в свою очередь, транзитивно добавляет jackson-annotations и jackson-core .
com.fasterxml.jackson.core jackson-databind 2.13.3
Мы также используем Lombok для обработки шаблонного кода для геттеров, сеттеров и конструкторов.
Базовая сериализация и десериализация JSON с Jackson
Давайте рассмотрим наиболее важные варианты использования Jackson с примерами кода.
Базовое преобразование POJO/JSON с использованием ObjectMapper
Давайте начнем с представления простого POJO под названием Employee:
@Getter @AllArgsConstructor @NoArgsConstructor public class Employee
Начнем с преобразования POJO в строку JSON:
public class JacksonTest < ObjectMapper objectMapper = new ObjectMapper(); @Test void pojoToJsonString() throws JsonProcessingException < Employee employee = new Employee("Mark", "James", 20); String json = objectMapper.writeValueAsString(employee); System.out.println(json); >>
В качестве вывода увидим следующее:
Теперь посмотрим, как преобразовать строку JSON в объект Employee с помощью ObjectMapper .
public class JacksonTest < . @Test void jsonStringToPojo() throws JsonProcessingException < String employeeJson = ""; Employee employee = objectMapper.readValue(employeeJson, Employee.class); assertThat(employee.getFirstName()).isEqualTo("Jalil"); > >
ObjectMapper также предлагает богатый API для чтения JSON из разных источников в разные форматы, давайте проверим самые важные из них.
Создание POJO из файла JSON
Это делается с помощью метода readValue() .
Файл JSON в тестовых ресурсах employee.json :
public class JacksonTest < . @Test void jsonFileToPojo() throws IOException < File file = new File("src/test/resources/employee.json"); Employee employee = objectMapper.readValue(file, Employee.class); assertThat(employee.getAge()).isEqualTo(44); assertThat(employee.getLastName()).isEqualTo("Simpson"); assertThat(employee.getFirstName()).isEqualTo("Homer"); >>
Создание POJO из массива байт в формате JSON
public class JacksonTest < . @Test void byteArrayToPojo() throws IOException < String employeeJson = ""; Employee employee = objectMapper.readValue(employeeJson.getBytes(), Employee.class); assertThat(employee.getFirstName()).isEqualTo("Jalil"); > >
Создание списка POJO из JSON
Иногда документ JSON представляет собой не объект, а список объектов. Давайте посмотрим, как можно его прочитать.
public class JacksonTest < . @Test void fileToListOfPojos() throws IOException < File file = new File("src/test/resources/employeeList.json"); ListemployeeList = objectMapper.readValue(file, new TypeReference<>()<>); assertThat(employeeList).hasSize(2); assertThat(employeeList.get(0).getAge()).isEqualTo(33); assertThat(employeeList.get(0).getLastName()).isEqualTo("Simpson"); assertThat(employeeList.get(0).getFirstName()).isEqualTo("Marge"); > >
Создание Map из JSON
Мы можем преобразовать JSON в Java Map , что очень удобно, если мы не знаем, чего ожидать от файла JSON, который мы пытаемся спарсить. ObjectMapper превратит имя каждой переменной в JSON в ключ для Map, а значение этой переменной — в значение по этому ключу.
public class JacksonTest < . @Test void fileToMap() throws IOException < File file = new File("src/test/resources/employee.json"); Mapemployee = objectMapper.readValue(file, new TypeReference<>()<>); assertThat(employee.keySet()).containsExactly("firstName", "lastName", "age"); assertThat(employee.get("firstName")).isEqualTo("Homer"); assertThat(employee.get("lastName")).isEqualTo("Simpson"); assertThat(employee.get("age")).isEqualTo(44); > >
Игнорирование неизвестных полей JSON
Иногда ожидаемый нами JSON может иметь дополнительные поля, не определенные в POJO. Поведение Jackson по умолчанию заключается в том, чтобы в таких случаях генерировать исключение UnrecognizedPropertyException . Однако же мы можем настроить Jackson так, чтобы он не расстраивался по поводу неизвестных полей и просто игнорировал их. Это делается путем установки FAIL_ON_UNKNOWN_PROPERTIES ObjectMapper в false.
public class JacksonTest < . @Test void fileToPojoWithUnknownProperties() throws IOException < File file = new File("src/test/resources/employeeWithUnknownProperties.json"); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Employee employee = objectMapper.readValue(file, Employee.class); assertThat(employee.getFirstName()).isEqualTo("Homer"); assertThat(employee.getLastName()).isEqualTo("Simpson"); assertThat(employee.getAge()).isEqualTo(44); >>
Работа с датами in Jackson
Преобразование дат может быть непростым занятием, поскольку они могут быть представлены во многих форматах и уровнях спецификации (секунды, миллисекунды и т. д.).
Дата в JSON
Прежде чем говорить о преобразовании дат и Jackson, нам нужно поговорить о новом Date API в Java 8. Он был введен для устранения недостатков более старых java.util.Date и java.util.Calendar . В основном нас интересует использование класса LocalDate , который предлагает эффективный способ представления даты и времени.
Для этого нам нужно добавить в Jackson дополнительный модуль, чтобы он мог обрабатывать LocalDate .
com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.13.3
Затем нам нужно сказать ObjectMapper найти и зарегистрировать новый модуль, который мы только что добавили.
public class JacksonTest < ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); . @Test void orderToJson() throws JsonProcessingException < Order order = new Order(1, LocalDate.of(1900,2,1)); String json = objectMapper.writeValueAsString(order); System.out.println(json); >>
В этом случае поведение Jackson по умолчанию состоит в том, чтобы показывать дату как [гггг-ММ-дд]. Таким образом, вывод будет
Однако мы можем указать Jackson, в каком формате нам нужна дата. Это можно сделать с помощью аннотации @JsonFormat .
public class Order < private int id; @JsonFormat(pattern = "dd/MM/yyyy") private LocalDate date; >
@Test void orderToJsonWithDate() throws JsonProcessingException
JSON в дату
Мы можем использовать ту же конфигурацию выше, чтобы преобразовать поле JSON в дату.
public class JacksonTest < . @Test void fileToOrder() throws IOException < File file = new File("src/test/resources/order.json"); Order order = objectMapper.readValue(file, Order.class); assertThat(order.getDate().getYear()).isEqualTo(2000); assertThat(order.getDate().getMonthValue()).isEqualTo(4); assertThat(order.getDate().getDayOfMonth()).isEqualTo(30); >>
Аннотации Jackson
Важную роль в настройке процесса преобразования JSON/POJO играют аннотации. Мы видели пример с преобразованием даты, где мы использовали аннотацию @JsonFormat . Аннотации влияют на то, как данные читаются, записываются или даже на то и другое. Давайте рассмотрим некоторые из этих аннотаций на основе их категорий.
Аннотации чтения
Они влияют на то, как Jackson преобразует JSON в POJO.
@JsonSetter
Это полезно, когда мы хотим сопоставить поле в строке JSON с полем в POJO, где их имена не совпадают.
@NoArgsConstructor @AllArgsConstructor @Getter public class Car
public class JacksonTest < . @Test void fileToCar() throws IOException < File file = new File("src/test/resources/car.json"); Car car = objectMapper.readValue(file, Car.class); assertThat(car.getBrand()).isEqualTo("BMW"); >>
@JsonAnySetter
Эта аннотация полезна в случаях, когда JSON содержит некоторые поля, не объявленные в POJO. Он используется с сеттером, который вызывается для каждого нераспознанного поля.
public class Car < @JsonSetter("carBrand") private String brand; private MapunrecognizedFields = new HashMap<>(); @JsonAnySetter public void allSetter(String fieldName, String fieldValue) < unrecognizedFields.put(fieldName, fieldValue); >>
public class JacksonTest < . @Test void fileToUnrecognizedCar() throws IOException < File file = new File("src/test/resources/carUnrecognized.json"); Car car = objectMapper.readValue(file, Car.class); assertThat(car.getUnrecognizedFields()).containsKey("productionYear"); >>
Аннотации записи
Они влияют на то, как Jackson преобразует POJO в JSON.
@JsonGetter
Это полезно, когда мы хотим сопоставить поле POJO с полем JSON, используя другое имя. Например, предположим, что у нас есть класс Cat с полем name , но мы хотим, чтобы его JSON-имя было catName .
@NoArgsConstructor @AllArgsConstructor public class Cat < private String name; @JsonGetter("catName") public String getName() < return name; >>
public class JacksonTest < . @Test void catToJson() throws JsonProcessingException < Cat cat = new Cat("Monica"); String json = objectMapper.writeValueAsString(cat); System.out.println(json); >>
@JsonAnyGetter
Эта аннотация позволяет нам использовать объект Map как источник свойств JSON. Скажем, у нас есть эта карта как поле в классе Cat .
@NoArgsConstructor @AllArgsConstructor public class Cat < private String name; @JsonAnyGetter Mapmap = Map.of( "name", "Jack", "surname", "wolfskin" ); . >
@Test void catToJsonWithMap() throws JsonProcessingException
Вывод будет следующим:
Аннотации чтения и записи
Эти аннотации влияют как на чтение, так и на запись JSON.
@JsonIgnore
Поле с аннотацией игнорируется как при записи, так и при чтении JSON.
@AllArgsConstructor @NoArgsConstructor @Getter public class Dog
public class JacksonTest < . @Test void dogToJson() throws JsonProcessingException < Dog dog = new Dog("Max", 3); String json = objectMapper.writeValueAsString(dog); System.out.println(json); >>
То же самое относится и к чтению в POJO.
Предположим, у нас есть файл dog.json :
public class JacksonTest < . @Test void fileToDog() throws IOException < File file = new File("src/test/resources/dog.json"); Dog dog = objectMapper.readValue(file, Dog.class); assertThat(dog.getName()).isEqualTo("bobby"); assertThat(dog.getAge()).isNull(); >>
У Jackson есть еще много полезных аннотаций, которые дают нам больше контроля над процессом сериализации/десериализации. Полный их список можно найти в репозитории Jackson на Github.
Резюме
- Jackson — одна из самых мощных и популярных библиотек для обработки JSON в Java.
- Jackson включает три основных модуля: Streaming API, Tree Model и Data Binding.
- Jackson предоставляет ObjectMapper, который легко настраивается в соответствии с потребностями, с возможностью задавать его свойства и использовать аннотации.
Все примеры кода лежат в репозитории на GitHub.
Приглашаем всех желающих на открытый урок «Реляционные базы данных для начинающих Java-разработчиков». На уроке поговорим о месте реляционных баз данных в архитектуре информационных систем. Рассмотрим основные компоненты и возможности РСУБД на примере PostgreSQL. Сделаем обзор основных технологий по работе с реляционными БД в Java (JDBC, JPA/Hibernate, Spring Data и др.). Регистрируйтесь по ссылке.
Аннотации Jackson
В этой статье мы рассмотрим аннотации Jackson.
Мы изучим как использовать существующие аннотации Jackson и как создать свои.
А также как их отключить.
2. Аннотации Jackson для сериализации
Для начала рассмотрим аннотации применяемые для сериализации объектов.
2.1 @JsonAnyGetter
Аннотация @JsonAnyGetter позволяет сериализовать Map как обычные свойства класса, без вложенности.
Пример — класс ExtendableBean имеет фиксированное свойство name и поле с типом Map для задания произвольного количества атрибутов в виде пар key/value:
public class ExtendableBean < public String name; private Mapproperties; @JsonAnyGetter public Map getProperties() < return properties; >>
Сериализация без аннотации @JsonAnyGetter:
С аннотацией @JsonAnyGetter Jackson сериализует атрибуты key-value как обычные свойствами объекта, без вложенности:
2.2. @JsonGetter
Аннотация @JsonGetter (альтернатива аннотации @JsonProperty), которой можно пометить определённый метод как геттер.
В следующем примере помечаем разные методы класса MyBean аннотациями @JsonGetter :
public class MyBean < private int id; private String name; // свойство name существует @JsonGetter("name") public String getTheName() < return name; >// название метода без get. @JsonGetter public String testMethod1() < return "testMethod1"; >// название метода с get. @JsonGetter public String getTestMethod2() < return "getTestMethod2"; >// // свойство myProperty не существует @JsonGetter("myProperty") public String testMethod3() < return "testMethod3"; >>
< "name": "BeanName", // getTheName -->name "testMethod1": "testMethod1", "testMethod2": "getTestMethod2", // getTestMethod2 --> testMethod2 "myProperty": "testMethod3" // testMethod3 --> myProperty >
2.3. @JsonPropertyOrder
Аннотация @JsonPropertyOrder используется для указания порядка свойств при сериализации объекта в JSON.
Давайте зададим свой порядок свойств класса MyBean:
@JsonPropertyOrder(< "name", "id" >) public class MyBean
2.4. @JsonRawValue
@JsonRawValue используется чтобы Jackson сериализовал свойство так как оно есть.
public class RawBean
2.5. @JsonValue
@JsonValue указывает на то что результат вызова метода должен использоваться как сериализация всего экземпляра класса.
Например для перечислений – мы помечаем метод getName аннотацией @JsonValue таким образом, что элементы перечисления сериализуются через значение свойства name:
public enum TypeEnumWithValue < TYPE1(1, "Type A"), TYPE2(2, "Type 2"); private Integer id; private String name; TypeEnumWithValue(Integer id, String name) < this.id = id; this.name = name; >@JsonValue public String getName() < return name; >>
// "TYPE1" - без @JsonValue // "Type A" - с @JsonValue @Test public void whenSerializingUsingJsonValue_thenCorrect() throws JsonParseException, IOException < String enumAsString = new ObjectMapper() .writeValueAsString(TypeEnumWithValue.TYPE1); // "Type A" assertThat(enumAsString, is("\"Type A\"")); >
2.6. @JsonRootName
Аннотация @JsonRootName используется, если включена вложенность (wrapping), для указания названия корневого элемента.
Вложенность означает, что при сериализации класса User вместо этого:
получим вот это:
Чтобы изменить название корневого элемента на «user» применим аннотацию @JsonRootName :
@JsonRootName(value = «user») public class UserWithRoot
По-умолчанию, названием корневого элемента будет название класса – UserWithRoot. Используя данную аннотацию, мы получим более удобное название — user:
@Test public void whenSerializingUsingJsonRootName_thenCorrect() throws JsonProcessingException
2.7. @JsonSerialize
@JsonSerialize применяется для указания класса, с помощью которого будет производиться сериализация сущности.
Рассмотрим пример – применим аннотацию @JsonSerialize для сериализации свойства eventDate с помощь класса CustomDateSerializer:
public class Event
Код простого кастомного сериализации Jackson:
public class CustomDateSerializer extends StdSerializer < private static SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss"); public CustomDateSerializer() < this(null); >public CustomDateSerializer(Class t) < super(t); >@Override public void serialize( Date value, JsonGenerator gen, SerializerProvider arg2) throws IOException, JsonProcessingException < gen.writeString(formatter.format(value)); >>
@Test public void whenSerializingUsingJsonSerialize_thenCorrect() throws JsonProcessingException, ParseException
3. Jackson Deserialization Annotations
Далее рассмотрим аннотации Jackson для десериализации, т.е. для обратного преобразования JSON в Java-объект.
3.1. @JsonCreator
Аннотация @JsonCreator используется для настройки конструктора или фабрики, которые будут применяться при десериализации.
Это очень полезно когда нам нужно десериализовать некий JSON, который не в полной мере соответствует десериализуемому объекту.
Тут пригодится пример. Скажем, нам нужно десериализовать следующий JSON:
Однако, в нашем целевом классе нет поля theName – в нём есть только поле name. При этом мы не хотим менять наш класс. Мы можем указать Jackson, как нужно десериализовывать наш класс, отметив конструктор аннотацией @JsonCreator и используя @JsonProperty для аргументов:
public class BeanWithCreator < public int id; public String name; @JsonCreator public BeanWithCreator( @JsonProperty("id") int id, @JsonProperty("theName") String name) < this.id = id; this.name = name; >>
@Test public void whenDeserializingUsingJsonCreator_thenCorrect() throws IOException < String json = ""; BeanWithCreator bean = new ObjectMapper() .readerFor(BeanWithCreator.class) .readValue(json); assertEquals("My bean", bean.name); >
3.2. @JacksonInject
@JacksonInject используется чтобы пометить поле, в которое нужно внедрить значение, а не брать из данных JSON. При этом внедрение можно выполнить как по типу данных, так и по названию свойства.
В ниже приведённом примере мы используем @JacksonInject для внедрения своего значения в свойство id:
public class BeanWithInject
Обратите внимание, что в JSON нет значения для id, однако в десериализованном объекте оно уже есть:
@Test public void whenDeserializingUsingJsonInject_thenCorrect() throws IOException < String json = ""; InjectableValues inject = new InjectableValues.Std() .addValue(int.class, 1); BeanWithInject bean = new ObjectMapper().reader(inject) .forType(BeanWithInject.class) .readValue(json); assertEquals("My bean", bean.name); assertEquals(1, bean.id); >
3.3. @JsonAnySetter
@JsonAnySetter помечается метод, в который будут передаваться все нераспознанные поля и их значения. Помеченный метод должен иметь определенную сигнатуру. Таким образом можно складывать значение в Map.
Применим @JsonAnySetter для десериализации объектаExtendableBean:
public class ExtendableBean < public String name; private Mapproperties; @JsonAnySetter public void add(String key, String value) < properties.put(key, value); >>
JSON для десериализации:
Поле name попадёт в свойство name, а поля attr1 и attr2 попадут в метод add, т.к. в классе нет соответствующих свойств attr1, attr2.
@Test public void whenDeserializingUsingJsonAnySetter_thenCorrect() throws IOException < String json = ""; ExtendableBean bean = new ObjectMapper() .readerFor(ExtendableBean.class) .readValue(json); assertEquals("My bean", bean.name); assertEquals("val2", bean.getProperties().get("attr2")); >
3.4. @JsonSetter
@JsonSetter это альтернатива @JsonProperty – применяется для обозначения метода, который будет использоваться для установки значения свойству (сеттер).
Это очень полезно когда данные из JSON не совсем подходят под целевой класс или с данными нужно выполнить какие-то простые проверки или преобразования.
В следующем примере у нас есть метод setTheName() как сеттер свойства name в классе MyBean:
public class MyBean < public int id; private String name; @JsonSetter("name") public void setTheName(String name) < this.name = name; >>
Метод setTheName() будет использоваться для десериализации свойства name:
@Test public void whenDeserializingUsingJsonSetter_thenCorrect() throws IOException < String json = ""; MyBean bean = new ObjectMapper() .readerFor(MyBean.class) .readValue(json); assertEquals("My bean", bean.getTheName()); >
3.5. @JsonDeserialize
@JsonDeserialize используется для указания использовать указанный пользовательский десериализатор.
Пометим свойство eventDate аннотацией @JsonDeserialize для десериализации с помощью класса CustomDateDeserializer:
public class Event
Код нашего десериализатора:
public class CustomDateDeserializer extends StdDeserializer < private static SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss"); public CustomDateDeserializer() < this(null); >public CustomDateDeserializer(Class vc) < super(vc); >@Override public Date deserialize( JsonParser jsonparser, DeserializationContext context) throws IOException < String date = jsonparser.getText(); try < return formatter.parse(date); >catch (ParseException e) < throw new RuntimeException(e); >> >
@Test public void whenDeserializingUsingJsonDeserialize_thenCorrect() throws IOException < String json = ""; SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss"); Event event = new ObjectMapper() .readerFor(Event.class) .readValue(json); assertEquals( "20-12-2014 02:30:00", df.format(event.eventDate)); >
4. Jackson Property Inclusion Annotations
4.1. @JsonIgnoreProperties
@JsonIgnoreProperties – one of the most common annotations in Jackson – is used to mark a property or a list of properties to be ignored at the class level.
Let’s go over a quick example ignoring the property id from serialization:
@JsonIgnoreProperties(< "id" >) public class BeanWithIgnore
And here’s the test making sure the ignore happens:
@Test public void whenSerializingUsingJsonIgnoreProperties_thenCorrect() throws JsonProcessingException
4.2. @JsonIgnore
The @JsonIgnore annotation is used to mark a property to be ignored at the field level.
Let’s use @JsonIgnore to ignore the property id from serialization:
public class BeanWithIgnore
And the test making sure that id was successfully ignored:
@Test public void whenSerializingUsingJsonIgnore_thenCorrect() throws JsonProcessingException
4.3. @JsonIgnoreType
@JsonIgnoreType is used to mark all properties of an annotated type to be ignored.
Let’s use the annotation to mark all properties of type Name to be ignored:
public class User < public int id; public Name name; @JsonIgnoreType public static class Name < public String firstName; public String lastName; >>
Here’s the simple test making sure the ignore works correctly:
@Test public void whenSerializingUsingJsonIgnoreType_thenCorrect() throws JsonProcessingException, ParseException
4.4. @JsonInclude
@JsonInclude is used to exclude properties with empty/null/default values.
Let’s look at an example – excluding nulls from serialization:
@JsonInclude(Include.NON_NULL) public class MyBean
Here’s the full test:
public void whenSerializingUsingJsonInclude_thenCorrect() throws JsonProcessingException
4.5. @JsonAutoDetect
@JsonAutoDetect is used to override the default semantics of which properties are visible and which are not.
Let’s take a look at how the annotation can be very helpful with a simple example – let’s enable serializing private properties:
@JsonAutoDetect(fieldVisibility = Visibility.ANY) public class PrivateBean
@Test public void whenSerializingUsingJsonAutoDetect_thenCorrect() throws JsonProcessingException
5. Jackson Polymorphic Type Handling Annotations
Next – let’s take a look at Jackson polymorphic type handling annotations:
- @JsonTypeInfo is used to indicate details of what type information is included in serialization
- @JsonSubTypes is used to indicate sub-types of annotated type
- @JsonTypeName is used to define logical type name to use for annotated class
Let’s look at a more complex example and use all three – @JsonTypeInfo, @JsonSubTypes, and @JsonTypeName – to serialize/deserialize the entity Zoo:
public class Zoo < public Animal animal; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = As.PROPERTY, property = "type") @JsonSubTypes(< @JsonSubTypes.Type(value = Dog.class, name = "dog"), @JsonSubTypes.Type(value = Cat.class, name = "cat") >) public static class Animal < public String name; >@JsonTypeName("dog") public static class Dog extends Animal < public double barkVolume; >@JsonTypeName("cat") public static class Cat extends Animal < boolean likesCream; public int lives; >>
When we do serialization:
@Test public void whenSerializingPolymorphic_thenCorrect() throws JsonProcessingException
Here’s what serializing the Zoo instance with the Dog will result in:
Now for de-serialization – let’s start with the following JSON input:
And let’s see how that gets unmarshalled to a Zoo instance:
6. Jackson General Annotations
Next – let’s discuss some of Jackson more general annotations.
6.1. @JsonProperty
@JsonProperty is used to indicate the property name in JSON.
Let’s go over the annotation with a simple example – and use @JsonProperty to serialize/deserialize the property name when we’re dealing with non-standard getters and setters:
public class MyBean < public int id; private String name; @JsonProperty("name") public void setTheName(String name) < this.name = name; >@JsonProperty("name") public String getTheName() < return name; >>
@Test public void whenUsingJsonProperty_thenCorrect() throws IOException
6.2. @JsonFormat
The @JsonFormat annotation can be used to specify a format when serializing Date/Time values.
In the following example – we use @JsonFormat to control the format of the property eventDate:
public class Event
And here’s the test:
@Test public void whenSerializingUsingJsonFormat_thenCorrect() throws JsonProcessingException, ParseException
6.3. @JsonUnwrapped
@JsonUnwrapped is used for defining values that should be unwrapped/flattened when serialized/deserialized.
Let’s see exactly how that works; we’ll use the annotation to unwrap the property name:
public class UnwrappedUser < public int id; @JsonUnwrapped public Name name; public static class Name < public String firstName; public String lastName; >>
Let’s now serialize an instance of this class:
@Test public void whenSerializingUsingJsonUnwrapped_thenCorrect() throws JsonProcessingException, ParseException
Here’s how the output looks like – the fields of the static nested class unwrapped along with the other field:
6.4. @JsonView
@JsonView is used to indicate the View in which the property will be included for serialization/deserialization.
An example will show exactly how that works – we’ll use @JsonView to serialize an instance of Item entity.
Let’s start with the views:
public class Views < public static class Public <>public static class Internal extends Public <> >
And now here’s the Item entity, using the views:
public class Item
Finally – the full test:
@Test public void whenSerializingUsingJsonView_thenCorrect() throws JsonProcessingException
6.5. @JsonManagedReference, @JsonBackReference
The @JsonManagedReference and @JsonBackReference annotations are used to handle parent/child relationships and work around loops.
In the following example – we use @JsonManagedReference and @JsonBackReference to serialize our ItemWithRef entity:
public class ItemWithRef
Our UserWithRef entity:
public class UserWithRef < public int id; public String name; @JsonBackReference public ListuserItems; >
@Test public void whenSerializingUsingJacksonReferenceAnnotation_thenCorrect() throws JsonProcessingException
6.6. @JsonIdentityInfo
@JsonIdentityInfo is used to indicate that Object Identity is to be used when serializing/deserializing values – for instance, to deal with infinite recursion type of problems.
In the following example – we have an ItemWithIdentity entity with a bidirectional relationship with the UserWithIdentity entity:
@JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = «id») public class ItemWithIdentity
And the UserWithIdentity entity:
@JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class UserWithIdentity < public int id; public String name; public ListuserItems; >
Now, let’s see how the infinite recursion problem is handled:
@Test public void whenSerializingUsingJsonIdentityInfo_thenCorrect() throws JsonProcessingException
Here’s the full output of the serialized item and user:
6.7. @JsonFilter
The @JsonFilter annotation specifies a filter to be used during serialization.
Let’s take a look at an example; first, we define the entity, and we point to the filter:
@JsonFilter(«myFilter») public class BeanWithFilter
Now, in the full test, we define the filter – which excludes all other properties except name from serialization:
@Test public void whenSerializingUsingJsonFilter_thenCorrect() throws JsonProcessingException
7. Custom Jackson Annotation
Next – let’s see how to create a custom Jackson annotation; we can make use of the @JacksonAnnotationsInside annotation – as in the following example:
@Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonInclude(Include.NON_NULL) @JsonPropertyOrder(< "name", "id", "dateCreated" >) public @interface CustomAnnotation
Now, if we use the new annotation on an entity:
@CustomAnnotation public class BeanWithCustomAnnotation
We can see how it does combine the existing annotations into a simpler, custom one that we can use as a shorthand:
@Test public void whenSerializingUsingCustomAnnotation_thenCorrect() throws JsonProcessingException
The output of the serialization process:
8. Jackson MixIn Annotations
Next – let’s see how to use Jackson MixIn annotations.
Let’s use the MixIn annotations to – for example – ignore properties of type User:
public class Item < public int id; public String itemName; public User owner; >@JsonIgnoreType public class MyMixInForIgnoreType
Let’s see this in action:
@Test public void whenSerializingUsingMixInAnnotation_thenCorrect() throws JsonProcessingException
9. Disable Jackson Annotation
Finally – let’s see how we can disable all Jackson annotations.We can do this by disabling the MapperFeature.USE_ANNOTATIONS as in the following example:
@JsonInclude(Include.NON_NULL) @JsonPropertyOrder(< "name", "id" >) public class MyBean
Now, after disabling annotations, these should have no effect and the defaults of the library should apply:
@Test public void whenDisablingAllAnnotations_thenAllDisabled() throws IOException
The result of serialization before disabling annotations:
The result of serialization after disabling annotations:
10. Заключение
Мы рассмотрели аннотации Jackson, которые при правильном использовании дают вам гибкость при сериализации/десериализации объектов.
Видео от автора оригинальной статьи:
Код примеров может быть найден по ссылке GitHub project – это maven-проект, и поэтому вам будет его легко импортировать и запустить.
Статья является творческим переводом оригинальной статьи.