Большая часть веба на сегодняшний день обменивается данными в формате 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
.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
Мы также используем Lombok для обработки шаблонного кода для геттеров, сеттеров и конструкторов.
Базовая сериализация и десериализация JSON с Jackson
Давайте рассмотрим наиболее важные варианты использования Jackson с примерами кода.
Базовое преобразование POJO/JSON с использованием ObjectMapper
Давайте начнем с представления простого POJO под названием Employee:
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private String firstName;
private String lastName;
private int age;
}
Начнем с преобразования 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);
}
}
В качестве вывода увидим следующее:
{"firstName":"Mark","lastName":"James","age":20}
Теперь посмотрим, как преобразовать строку JSON в объект Employee
с помощью ObjectMapper
.
public class JacksonTest {
...
@Test
void jsonStringToPojo() throws JsonProcessingException {
String employeeJson = "{\n" +
" \"firstName\" : \"Jalil\",\n" +
" \"lastName\" : \"Jarjanazy\",\n" +
" \"age\" : 30\n" +
"}";
Employee employee = objectMapper.readValue(employeeJson, Employee.class);
assertThat(employee.getFirstName()).isEqualTo("Jalil");
}
}
ObjectMapper также предлагает богатый API для чтения JSON из разных источников в разные форматы, давайте проверим самые важные из них.
Создание POJO из файла JSON
Это делается с помощью метода readValue()
.
Файл JSON в тестовых ресурсах employee.json
:
{
"firstName":"Homer",
"lastName":"Simpson",
"age":44
}
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 = "{\n" +
" \"firstName\" : \"Jalil\",\n" +
" \"lastName\" : \"Jarjanazy\",\n" +
" \"age\" : 30\n" +
"}";
Employee employee = objectMapper.readValue(employeeJson.getBytes(), Employee.class);
assertThat(employee.getFirstName()).isEqualTo("Jalil");
}
}
Создание списка POJO из JSON
Иногда документ JSON представляет собой не объект, а список объектов. Давайте посмотрим, как можно его прочитать.
employeeList.json
:
[
{
"firstName":"Marge",
"lastName":"Simpson",
"age":33
},
{
"firstName":"Homer",
"lastName":"Simpson",
"age":44
}
]
public class JacksonTest {
...
@Test
void fileToListOfPojos() throws IOException {
File file = new File("src/test/resources/employeeList.json");
List<Employee> employeeList = 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");
Map<String, Object> employee = 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.
employeeWithUnknownProperties.json
:
{
"firstName":"Homer",
"lastName":"Simpson",
"age":44,
"department": "IT"
}
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
.
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.3</version>
</dependency>
Затем нам нужно сказать 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 по умолчанию состоит в том, чтобы показывать дату как [гггг-ММ-дд]. Таким образом, вывод будет {"id":1,"date":[1900,2,1]}
Однако мы можем указать Jackson, в каком формате нам нужна дата. Это можно сделать с помощью аннотации @JsonFormat
.
public class Order {
private int id;
@JsonFormat(pattern = "dd/MM/yyyy")
private LocalDate date;
}
@Test
void orderToJsonWithDate() throws JsonProcessingException {
Order order = new Order(1, LocalDate.of(2023, 1, 1));
String json = objectMapper.writeValueAsString(order);
System.out.println(json);
}
Это должно вывести {"id":1,"date":"01/01/2023"}
.
JSON в дату
Мы можем использовать ту же конфигурацию выше, чтобы преобразовать поле JSON в дату.
order.json
:
{
"id" : 1,
"date" : "30/04/2000"
}
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 {
@JsonSetter("carBrand")
private String brand;
}
{
"carBrand" : "BMW"
}
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 Map<String, String> unrecognizedFields = new HashMap<>();
@JsonAnySetter
public void allSetter(String fieldName, String fieldValue) {
unrecognizedFields.put(fieldName, fieldValue);
}
}
файл carUnrecognized.json
:
{
"carBrand" : "BMW",
"productionYear": 1996
}
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);
}
}
Это выведет
{
"catName":"Monica"
}
@JsonAnyGetter
Эта аннотация позволяет нам использовать объект Map
как источник свойств JSON. Скажем, у нас есть эта карта как поле в классе Cat
.
@NoArgsConstructor
@AllArgsConstructor
public class Cat {
private String name;
@JsonAnyGetter
Map<String, String> map = Map.of(
"name", "Jack",
"surname", "wolfskin"
);
...
}
@Test
void catToJsonWithMap() throws JsonProcessingException {
Cat cat = new Cat("Monica");
String json = objectMapper.writeValueAsString(cat);
System.out.println(json);
}
Вывод будет следующим:
{
"catName":"Monica",
"name":"Jack",
"surname":"wolfskin"
}
Аннотации чтения и записи
Эти аннотации влияют как на чтение, так и на запись JSON.
@JsonIgnore
Поле с аннотацией игнорируется как при записи, так и при чтении JSON.
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Dog {
private String name;
@JsonIgnore
private int age;
}
public class JacksonTest {
...
@Test
void dogToJson() throws JsonProcessingException {
Dog dog = new Dog("Max", 3);
String json = objectMapper.writeValueAsString(dog);
System.out.println(json);
}
}
Это выведет {"name":"Max"}
То же самое относится и к чтению в POJO.
Предположим, у нас есть файл dog.json
:
{
"name" : "bobby",
"age" : 5
}
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 и др.). Регистрируйтесь по ссылке.