1. Введение в Jackson
Обмен данными между Java-приложением и внешним миром очень часто можно представить как такую задачу: «Как превратить объект Java в JSON — и обратно?» Можно, конечно, написать свой парсер на String.split и регулярках (и даже получить удовольствие от страданий), но в реальных проектах так никто не делает.
В Java для работы с JSON есть несколько популярных библиотек. Главная из них — Jackson. Она настолько популярна, что входит в состав Spring Boot и используется во множестве других фреймворков и библиотек.
Что такое Jackson?
Jackson — это мощная и гибкая библиотека для сериализации (преобразования Java-объекта в JSON) и десериализации (обратная операция). Она состоит из нескольких модулей, но для 90% задач вам понадобятся два:
- jackson-core — ядро, низкоуровневый парсер.
- jackson-databind — высокоуровневый модуль, который умеет превращать Java-объекты в JSON и обратно.
Всё, что вам нужно знать для начала: если вы видите класс ObjectMapper — это Jackson.
Jackson считают стандартом де-факто для работы с JSON в Java, потому что он сочетает простоту и мощь. Чтобы сериализовать или десериализовать данные, достаточно буквально пары строк кода, и это делает библиотеку удобной даже для новичков. При этом она не ограничивается базовыми возможностями: благодаря аннотациям и множеству настроек можно гибко управлять тем, как именно данные будут превращаться в объекты и обратно, будь то коллекции, вложенные сущности или даты в разных форматах.
Немаловажно и то, что Jackson работает очень быстро и экономно, что критично для реальных проектов с большим количеством данных. Разработчики библиотеки поддерживают новые версии Java и актуальные изменения в стандарте JSON — Jackson остаётся надёжным выбором как для простых приложений, так и для крупных корпоративных систем.
2. Чтение JSON (десериализация)
Давайте попробуем прочитать JSON-строку и превратить её в Java-объект. Для этого нам понадобятся:
- Класс данных (например, Person)
- Класс ObjectMapper из Jackson
Подключение Jackson
Если вы используете Maven, добавьте в pom.xml:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
Если Gradle — аналогично:
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
Пример класса
public class Person {
public String name;
public int age;
}
Примечание: Для простоты поля сделаны публичными. Позже мы обсудим работу с приватными полями и геттерами/сеттерами.
Пример JSON
{
"name": "Alice",
"age": 30
}
Десериализация: превращаем JSON в объект
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
String json = "{\"name\": \"Alice\", \"age\": 30}";
ObjectMapper mapper = new ObjectMapper();
Person person = mapper.readValue(json, Person.class);
System.out.println(person.name); // Alice
System.out.println(person.age); // 30
}
}
Здесь Jackson парсит JSON-строку, находит поля, совпадающие по имени с полями или геттерами класса, и заполняет объект соответствующими значениями с помощью readValue.
Десериализация списка объектов
Допустим, у нас есть массив:
[
{ "name": "Bob", "age": 22 },
{ "name": "Eve", "age": 27 }
]
Десериализуем в список:
import com.fasterxml.jackson.core.type.TypeReference;
// ...
String json = "[{\"name\": \"Bob\", \"age\": 22}, {\"name\": \"Eve\", \"age\": 27}]";
ObjectMapper mapper = new ObjectMapper();
List<Person> people = mapper.readValue(json, new TypeReference<List<Person>>() {});
for (Person p : people) {
System.out.println(p.name + " (" + p.age + ")");
}
Возможно, у вас возник вопрос: а почему нельзя просто написать mapper.readValue(json, List.class)? Помните, мы недавно выяснили, что дженерики в Java стираются во время компиляции? Нам поэтому нужен TypeReference, чтобы Jackson понял, что внутри списка должны быть объекты типа Person.
3. Запись JSON (сериализация)
Теперь давайте проделаем обратную операцию: превратим Java-объект в JSON-строку.
Пример
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
String json = mapper.writeValueAsString(person);
System.out.println(json);
// {"name":"Charlie","age":40}
Сериализация списка объектов
ObjectMapper mapper = new ObjectMapper();
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", 25));
people.add(new Person("Dmitry", 31));
String json = mapper.writeValueAsString(people);
System.out.println(json);
// [{"name":"Anna","age":25},{"name":"Dmitry","age":31}]
Запись в файл
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
mapper.writeValue(new File("person.json"), person);
// Файл person.json теперь содержит JSON-объект
Красивый (pretty) JSON
По умолчанию Jackson пишет всё в одну строку. Но есть вариант и более «человекоориентированного» написания — pretty printing. Это значит, что JSON-строка выводится в читаемом виде: с отступами, переносами строк и аккуратным форматированием.
В отличие от «обычного» JSON, который чаще всего пишется в одну строку ради компактности, «красивый» вариант нужен для людей — чтобы легко просматривать структуру данных в логах, файлах или на экране.
ObjectMapper mapper = new ObjectMapper();
Person person = new Person();
person.name = "Charlie";
person.age = 40;
String prettyJson = mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(person);
System.out.println(prettyJson);
/*
{
"name" : "Charlie",
"age" : 40
}
*/
4. Аннотации Jackson
Jackson поддерживает множество аннотаций, которые позволяют управлять процессом сериализации и десериализации. Вот самые полезные:
@JsonProperty
Позволяет указать имя поля в JSON, если оно отличается от имени поля в классе.
import com.fasterxml.jackson.annotation.JsonProperty;
public class Person {
@JsonProperty("full_name")
public String name;
public int age;
}
{"full_name": "Olga", "age": 28}
Jackson поймёт, что поле full_name из JSON нужно записать в поле name объекта.
@JsonIgnore
Если вы не хотите сериализовать или десериализовать какое-то поле:
import com.fasterxml.jackson.annotation.JsonIgnore;
public class Person {
public String name;
@JsonIgnore
public int age;
}
В JSON не будет поля age, даже если оно есть в объекте.
@JsonInclude
Управляет тем, какие поля попадут в JSON. Например, сериализовать только непустые поля:
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
public String name;
public Integer age;
}
Если age == null, в JSON не будет ключа "age".
@JsonFormat
Позволяет задать формат для сериализации дат и времени.
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
public class Event {
public String title;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
public Date date;
}
Event event = new Event();
event.title = "Hackathon";
event.date = new Date();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(event);
// {"title":"Hackathon","date":"2024-06-07 15:23:00"}
Пример: всё вместе
import com.fasterxml.jackson.annotation.*;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
@JsonProperty("full_name")
private String name;
private int age;
@JsonIgnore
private String password;
// Геттеры и сеттеры обязательны для приватных полей!
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
5. Практика: сериализация и десериализация с учётом аннотаций
Давайте расширим ваше учебное приложение — теперь у нас есть класс User с приватными полями, датой регистрации и паролем, который не должен попадать в JSON.
import com.fasterxml.jackson.annotation.*;
import java.util.Date;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
@JsonProperty("login")
private String username;
private int age;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date registered;
@JsonIgnore
private String password;
// Геттеры и сеттеры обязательны!
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Date getRegistered() { return registered; }
public void setRegistered(Date registered) { this.registered = registered; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
Сериализация
import com.fasterxml.jackson.databind.ObjectMapper;
import java.text.SimpleDateFormat;
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.setUsername("superuser");
user.setAge(42);
user.setRegistered(new SimpleDateFormat("yyyy-MM-dd").parse("2024-06-07"));
user.setPassword("qwerty123"); // Не будет в JSON!
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
System.out.println(json);
/*
{
"login" : "superuser",
"age" : 42,
"registered" : "2024-06-07"
}
*/
Десериализация
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
String json = "{ \"login\": \"superuser\", \"age\": 42, \"registered\": \"2024-06-07\" }";
User user = mapper.readValue(json, User.class);
System.out.println(user.getUsername()); // superuser
System.out.println(user.getAge()); // 42
System.out.println(user.getRegistered());// Fri Jun 07 00:00:00 ...
System.out.println(user.getPassword()); // null (и это хорошо!)
6. Типичные ошибки при работе с Jackson
Ошибка №1: Отсутствует конструктор без параметров.
Jackson не сможет создать объект класса, если у него нет конструктора без параметров. Часто это бывает в классах, где явно объявлен только конструктор с аргументами.
Ошибка №2: Приватные поля без геттеров/сеттеров.
Если вы сделали все поля приватными, но забыли добавить геттеры и сеттеры, Jackson не сможет их заполнить при десериализации (по умолчанию).
Ошибка №3: Несовпадение имён полей.
Когда имя поля в JSON отличается от имени поля/геттера в Java, Jackson не найдёт соответствия. Используйте @JsonProperty.
Ошибка №4: Не тот формат даты.
Если формат даты в JSON не совпадает с ожидаемым форматом Java, Jackson выдаст ошибку парсинга. Используйте @JsonFormat для настройки.
Ошибка №5: Попытка сериализовать поля с аннотацией @JsonIgnore.
Такие поля не попадут в JSON — это не баг, а фича.
Ошибка №6: Сериализация/десериализация коллекций без указания типа.
Если не использовать TypeReference, Jackson не поймёт, какого типа объекты внутри коллекции.
Ошибка №7: Исключение при чтении/записи файла.
Не забудьте обрабатывать IOException при работе с файлами.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ