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 під час роботи з файлами.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ