JavaRush /Курси /JAVA 25 SELF /Jackson — читання та запис JSON, анотації

Jackson — читання та запис JSON, анотації

JAVA 25 SELF
Рівень 46 , Лекція 1
Відкрита

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 під час роботи з файлами.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ