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.
GO TO FULL VERSION