CodeGym /Kursy /JAVA 25 SELF /Jackson — odczyt i zapis JSON, adnotacje

Jackson — odczyt i zapis JSON, adnotacje

JAVA 25 SELF
Poziom 46 , Lekcja 1
Dostępny

1. Wprowadzenie do Jacksona

Wymianę danych między aplikacją Java a światem zewnętrznym bardzo często można sprowadzić do pytania: „Jak zamienić obiekt Javy na JSON — i z powrotem?”. Można oczywiście napisać własny parser na String.split i wyrażeniach regularnych (i nawet czerpać przyjemność z cierpienia), ale w realnych projektach nikt tak nie robi.

W Javie do pracy z JSON istnieje kilka popularnych bibliotek. Najważniejsza z nich — Jackson. Jest na tyle popularna, że wchodzi w skład Spring Boot i jest używana w wielu innych frameworkach i bibliotekach.

Czym jest Jackson?

Jackson to potężna i elastyczna biblioteka do serializacji (przekształcania obiektu Javy do JSON) i deserializacji (operacja odwrotna). Składa się z kilku modułów, ale w 90% przypadków potrzebne będą dwa:

  • jackson-core — rdzeń, niskopoziomowy parser.
  • jackson-databind — moduł wysokiego poziomu, który potrafi zamieniać obiekty Javy na JSON i z powrotem.

Wszystko, co musisz wiedzieć na początek: jeśli widzisz klasę ObjectMapper — to Jackson.

Jackson jest uznawany za de facto standard pracy z JSON w Javie, ponieważ łączy prostotę i moc. Aby zserializować lub zdeserializować dane, wystarczą dosłownie dwie linijki kodu, co czyni bibliotekę wygodną nawet dla początkujących. Jednocześnie nie ogranicza się do podstawowych możliwości: dzięki adnotacjom i wielu ustawieniom można elastycznie sterować tym, jak dane będą zamieniane na obiekty i z powrotem — czy to kolekcje, zagnieżdżone encje, czy daty w różnych formatach.

Co równie ważne, Jackson działa bardzo szybko i oszczędnie, co jest krytyczne w realnych projektach z dużą ilością danych. Twórcy biblioteki wspierają nowe wersje Javy i aktualne zmiany w standardzie JSON — Jackson pozostaje niezawodnym wyborem zarówno dla prostych aplikacji, jak i dużych systemów korporacyjnych.

2. Odczyt JSON (deserializacja)

Spróbujmy odczytać ciąg JSON i zamienić go na obiekt Javy. Do tego będą potrzebne:

  • Klasa danych (na przykład Person)
  • Klasa ObjectMapper z Jacksona

Dodanie Jacksona

Jeśli używasz Maven, dodaj do pom.xml:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.0</version>
</dependency>

Jeśli Gradle — analogicznie:

implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'

Przykład klasy

public class Person {
    public String name;
    public int age;
}

Uwaga: Dla prostoty pola są publiczne. Później omówimy pracę z polami prywatnymi i getterami/setterami.

Przykładowy JSON

{
  "name": "Alice",
  "age": 30
}

Deserializacja: zamieniamy JSON na obiekt

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
    }
}

Tutaj Jackson parsuje łańcuch JSON, znajduje pola odpowiadające nazwom pól lub getterów w klasie i wypełnia obiekt odpowiednimi wartościami za pomocą readValue.

Deserializacja listy obiektów

Załóżmy, że mamy tablicę:

[
  { "name": "Bob", "age": 22 },
  { "name": "Eve", "age": 27 }
]

Deserializujemy do listy:

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 + ")");
}

Być może masz pytanie: dlaczego nie można po prostu napisać mapper.readValue(json, List.class)? Pamiętasz, że typy generyczne w Javie są wymazywane na etapie kompilacji? Dlatego potrzebujemy TypeReference, aby Jackson zrozumiał, że wewnątrz listy mają być obiekty typu Person.

3. Zapis JSON (serializacja)

Teraz wykonamy operację odwrotną: zamienimy obiekt Javy na łańcuch JSON.

Przykład

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}

Serializacja listy obiektów

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}]

Zapis do pliku

ObjectMapper mapper = new ObjectMapper();

Person person = new Person();
person.name = "Charlie";
person.age = 40;

mapper.writeValue(new File("person.json"), person);
// Plik person.json zawiera teraz obiekt JSON

Czytelny (pretty) JSON

Domyślnie Jackson zapisuje wszystko w jednej linii. Jest jednak wariant bardziej „przyjazny dla człowieka” — pretty printing. Oznacza to, że łańcuch JSON jest wypisywany w czytelnej postaci: z wcięciami, łamaniami linii i schludnym formatowaniem.

W przeciwieństwie do „zwykłego” JSON, który najczęściej zapisuje się w jednej linii dla oszczędności miejsca, „ładny” wariant jest dla ludzi — aby łatwo przeglądać strukturę danych w logach, plikach lub na ekranie.

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. Adnotacje Jacksona

Jackson obsługuje wiele adnotacji, które pozwalają sterować procesem serializacji i deserializacji. Oto te najbardziej przydatne:

@JsonProperty

Pozwala wskazać nazwę pola w JSON, jeśli różni się od nazwy pola w klasie.

import com.fasterxml.jackson.annotation.JsonProperty;

public class Person {
    @JsonProperty("full_name")
    public String name;

    public int age;
}
{"full_name": "Olga", "age": 28}

Jackson zrozumie, że pole full_name z JSON należy zapisać do pola name obiektu.

@JsonIgnore

Jeśli nie chcesz serializować ani deserializować jakiegoś pola:

import com.fasterxml.jackson.annotation.JsonIgnore;

public class Person {
    public String name;

    @JsonIgnore
    public int age;
}

W JSON nie będzie pola age, nawet jeśli istnieje w obiekcie.

@JsonInclude

Steruje tym, które pola trafią do JSON. Na przykład serializuj tylko niepuste pola:

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
    public String name;
    public Integer age;
}

Jeśli age == null, w JSON nie będzie klucza "age".

@JsonFormat

Pozwala ustawić format serializacji dat i czasu.

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"}

Przykład: wszystko razem

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;

    // Gettery i settery są wymagane dla pól prywatnych!
    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. Praktyka: serializacja i deserializacja z uwzględnieniem adnotacji

Rozszerzmy waszą aplikację ćwiczeniową — mamy teraz klasę User z prywatnymi polami, datą rejestracji i hasłem, które nie powinno trafiać do 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;

    // Gettery i settery są obowiązkowe!
    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; }
}

Serializacja

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"); // Nie trafi do JSON!

String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);

System.out.println(json);
/*
{
  "login" : "superuser",
  "age" : 42,
  "registered" : "2024-06-07"
}
*/

Deserializacja

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 (i to dobrze!)

6. Typowe błędy przy pracy z Jacksonem

Błąd nr 1: Brak konstruktora bezargumentowego.
Jackson nie będzie w stanie utworzyć obiektu klasy, jeśli nie ma konstruktora bez parametrów. Często zdarza się to w klasach, w których jawnie zadeklarowano tylko konstruktor z argumentami.

Błąd nr 2: Prywatne pola bez getterów/setterów.
Jeśli wszystkie pola są prywatne, ale zapomniałeś dodać getterów i setterów, Jackson nie będzie mógł ich wypełnić podczas deserializacji (domyślnie).

Błąd nr 3: Niezgodność nazw pól.
Gdy nazwa pola w JSON różni się od nazwy pola/gettera w Javie, Jackson nie znajdzie dopasowania. Użyj @JsonProperty.

Błąd nr 4: Nieprawidłowy format daty.
Jeśli format daty w JSON nie zgadza się z oczekiwanym formatem w Javie, Jackson zgłosi błąd parsowania. Użyj @JsonFormat do konfiguracji.

Błąd nr 5: Próba serializowania pól z adnotacją @JsonIgnore.
Takie pola nie trafią do JSON — to nie bug, tylko feature.

Błąd nr 6: Serializacja/deserializacja kolekcji bez wskazania typu.
Jeśli nie użyjesz TypeReference, Jackson nie zrozumie, jakiego typu obiekty znajdują się wewnątrz kolekcji.

Błąd nr 7: Wyjątek podczas odczytu/zapisu pliku.
Nie zapominaj o obsłudze IOException podczas pracy z plikami.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION