CodeGym /Kursy /JAVA 25 SELF /Gson — serializacja i deserializacja, konfiguracja

Gson — serializacja i deserializacja, konfiguracja

JAVA 25 SELF
Poziom 46 , Lekcja 2
Dostępny

1. Wprowadzenie do Gson

Znamy już Jackson i widzieliśmy, dlaczego jest uznawany za de facto standard pracy z JSON w Javie. Ale jest też inna biblioteka, która zdobyła ogromną popularność, szczególnie w świecie Androida — to Gson. Gson powstał w Google jako lekkie i proste rozwiązanie do serializacji i deserializacji obiektów Javy do JSON. Ceni się go za niski próg wejścia: żeby zacząć, prawie nic nie trzeba konfigurować — większość zadań działa dosłownie „out of the box”.

Kolejną zaletą Gson jest jego lekkość. Biblioteka zajmuje mało miejsca i nie ściąga wielu zależności, dlatego często używa się jej tam, gdzie rozmiar aplikacji jest krytyczny, np. na urządzeniach mobilnych. Gson stał się faktycznym standardem dla projektów na Androidzie — kompaktowość i prostota odgrywają tam kluczową rolę.

Przy okazji, nazwa Gson rozwija się jako Google JSON. Czasami w społeczności można spotkać żartobliwą etymologię — Genius’ Son („syn geniusza”), ale to oczywiście nieoficjalne. Po prostu gra słów.

Dodanie Gson do projektu

Jeśli używasz Mavena lub Gradle, po prostu dodaj zależność (wersja może się różnić):

Maven:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>

Gradle:

implementation 'com.google.code.gson:gson:2.10.1'

Nie uczyliśmy jeszcze budowania projektów, więc na początek możesz po prostu pobrać plik jar z oficjalnej strony Gson i dodać go do projektu.

2. Podstawowe operacje: serializacja i deserializacja

Sprawdźmy, jak serializować i deserializować obiekty za pomocą Gson na przykładzie prostej klasy.

Przykład: klasa User

// Klasa do przykładów
public class User {
    private String name;
    private int age;
    private boolean active;

    // Konstruktor
    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }

    // Gettery i settery (Gson używa ich w razie potrzeby)
    public String getName() { return name; }
    public int getAge() { return age; }
    public boolean isActive() { return active; }
}

Serializacja: obiekt → JSON

import com.google.gson.Gson;

public class GsonExample {
    public static void main(String[] args) {
        User user = new User("Alice", 25, true);

        Gson gson = new Gson();
        String json = gson.toJson(user);

        System.out.println(json);
        // {"name":"Alice","age":25,"active":true}
    }
}

Uwaga: pola są serializowane z ich nazwami zdefiniowanymi w klasie!

Deserializacja: JSON → obiekt

public class GsonExample {
    public static void main(String[] args) {
        String json = "{\"name\":\"Bob\",\"age\":30,\"active\":false}";

        Gson gson = new Gson();
        User user = gson.fromJson(json, User.class);

        System.out.println(user.getName()); // Bob
        System.out.println(user.getAge());  // 30
        System.out.println(user.isActive());// false
    }
}

Praca z listami obiektów

Gson pracuje z kolekcjami nieco trudniej niż Jackson, ale wszystko jest do ogarnięcia.

import java.util.List;
import java.util.Arrays;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;

public class GsonListExample {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("Alice", 25, true),
            new User("Bob", 30, false)
        );

        Gson gson = new Gson();
        String json = gson.toJson(users);
        System.out.println(json);
        // [{"name":"Alice","age":25,"active":true},{"name":"Bob","age":30,"active":false}]

        // Deserializacja listy
        Type userListType = new TypeToken<List<User>>(){}.getType();
        List<User> users2 = gson.fromJson(json, userListType);
        System.out.println(users2.get(0).getName()); // Alice
    }
}

Ważna uwaga: dla deserializacji kolekcji używaj TypeToken<>!

3. Konfiguracja Gson: GsonBuilder

Gson udostępnia elastyczną konfigurację przez klasę GsonBuilder. Z jej pomocą możesz włączyć pretty printing, serializację null, formatowanie dat i wiele więcej.

Przykład: pretty printing i serializacja null

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class GsonBuilderExample {
    public static void main(String[] args) {
        User user = new User("Charlie", 0, false);

        Gson gson = new GsonBuilder()
            .setPrettyPrinting()        // Ładny wydruk (wcięcia)
            .serializeNulls()           // Serializuj pola null
            .create();

        String json = gson.toJson(user);
        System.out.println(json);
        /*
        {
          "name": "Charlie",
          "age": 0,
          "active": false
        }
        */
    }
}

Formatowanie dat

Jeśli masz pola typu Date, domyślnie Gson serializuje je w specyficznym formacie. Można ustawić własny format:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.Date;

public class DateExample {
    private String event;
    private Date date;

    public DateExample(String event, Date date) {
        this.event = event;
        this.date = date;
    }
}

public class Main {
    public static void main(String[] args) {
        DateExample meeting = new DateExample("Team Meeting", new Date());

        Gson gson = new GsonBuilder()
            .setDateFormat("yyyy-MM-dd HH:mm:ss")
            .create();

        String json = gson.toJson(meeting);
        System.out.println(json);
        // {"event":"Team Meeting","date":"2024-06-10 13:45:23"}
    }
}

4. Adnotacje Gson: kontrola serializacji

Gson wspiera adnotacje dla bardziej precyzyjnej kontroli nad serializacją i deserializacją.

@SerializedName — zmiana nazwy pola

Jeśli chcesz, aby pole w JSON miało inną nazwę, użyj @SerializedName:

import com.google.gson.annotations.SerializedName;

public class User {
    @SerializedName("full_name")
    private String name;
    private int age;
    private boolean active;

    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }
}

Teraz podczas serializacji pole będzie się nazywać full_name:

User user = new User("Diana", 28, true);
String json = new Gson().toJson(user);
// {"full_name":"Diana","age":28,"active":true}

@Expose — serializacja tylko oznaczonych pól

Jeśli chcesz serializować tylko wybrane pola, użyj @Expose i skonfiguruj Gson:

import com.google.gson.annotations.Expose;

public class User {
    @Expose
    private String name;

    @Expose
    private int age;

    private boolean active; // nie jest serializowane

    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }
}

Tworzymy Gson z obsługą @Expose:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

Gson gson = new GsonBuilder()
    .excludeFieldsWithoutExposeAnnotation()
    .create();

User user = new User("Eve", 21, false);
String json = gson.toJson(user);
// {"name":"Eve","age":21}

@Since/@Until — warunkowa serializacja według wersji
Można używać @Since i @Until do serializowania pól tylko dla określonych wersji (rzadko używane w praktyce, ale warto wiedzieć).

5. Cechy i ograniczenia Gson

Praca z obiektami zagnieżdżonymi

Gson świetnie radzi sobie z obiektami zagnieżdżonymi:

public class Profile {
    private User user;
    private String bio;

    public Profile(User user, String bio) {
        this.user = user;
        this.bio = bio;
    }
}

Profile profile = new Profile(new User("Frank", 27, true), "Java developer");
String json = new Gson().toJson(profile);
// {"user":{"name":"Frank","age":27,"active":true},"bio":"Java developer"}

Praca z kolekcjami

Z serializacją kolekcji (List, Map) nie ma problemu, ale do deserializacji używaj TypeToken (patrz wyżej).

Ograniczenia Gson w porównaniu z Jacksonem

  • Brak wsparcia klas record w Javie (do najnowszych wersji)
  • Ograniczone wsparcie nowego API typów daty/czasu (np. LocalDate, LocalDateTime — wymagane niestandardowe adaptery)
  • Nie obsługuje adnotacji Jackson
  • Brak wsparcia złożonych struktur polimorficznych „out of the box”
  • Brak automatycznej obsługi dwukierunkowych odwołań (cykliczności)

Niestandardowe adaptery (TypeAdapter)

Jeśli standardowe możliwości to za mało, możesz napisać własny adapter do serializacji/deserializacji złożonych typów.

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;

public class BooleanAsIntAdapter extends TypeAdapter<Boolean> {
    @Override
    public void write(JsonWriter out, Boolean value) throws IOException {
        out.value(value ? 1 : 0);
    }

    @Override
    public Boolean read(JsonReader in) throws IOException {
        return in.nextInt() == 1;
    }
}

// Użycie:
Gson gson = new GsonBuilder()
    .registerTypeAdapter(Boolean.class, new BooleanAsIntAdapter())
    .create();

6. Praktyka: serializacja i deserializacja z konfiguracją

Rozwińmy twoją aplikację edukacyjną i dodajmy zapisywanie i wczytywanie listy użytkowników w formacie JSON.

Klasa User z adnotacjami

import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;

public class User {
    @Expose
    @SerializedName("full_name")
    private String name;

    @Expose
    private int age;

    private boolean active; // nie jest serializowane

    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }

    // gettery, settery...
}

Zapisujemy listę użytkowników do JSON

import java.util.List;
import java.util.Arrays;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class SaveUsers {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("Ivan", 23, true),
            new User("Olga", 19, false)
        );

        Gson gson = new GsonBuilder()
            .excludeFieldsWithoutExposeAnnotation()
            .setPrettyPrinting()
            .create();

        String json = gson.toJson(users);
        System.out.println(json);
        /*
        [
          {
            "full_name": "Ivan",
            "age": 23
          },
          {
            "full_name": "Olga",
            "age": 19
          }
        ]
        */
    }
}

Wczytujemy listę użytkowników z JSON

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;

public class LoadUsers {
    public static void main(String[] args) {
        String json = "[{\"full_name\":\"Ivan\",\"age\":23},{\"full_name\":\"Olga\",\"age\":19}]";

        Gson gson = new GsonBuilder()
            .excludeFieldsWithoutExposeAnnotation()
            .create();

        Type userListType = new TypeToken<List<User>>(){}.getType();
        List<User> users = gson.fromJson(json, userListType);

        for (User user : users) {
            System.out.println(user.getName() + " (" + user.getAge() + ")");
        }
        // Ivan (23)
        // Olga (19)
    }
}

7. Porównanie Gson i Jackson

Cecha Gson Jackson
Łatwość użycia +++++ (bardzo proste) +++ (nieco trudniejsze)
Rozmiar biblioteki Mały Większy
Szybkość Szybko, ale odrobinę wolniej Bardzo szybko
Elastyczność Średnia Wysoka (więcej ustawień)
Obsługa adnotacji Własne (@SerializedName) Własne (@JsonProperty i inne)
Obsługa nowych typów Ograniczona Świetna (Java 8+, record)
Wsparcie dla Androida Świetna Dobra, ale cięższa
Praca z datami Tylko przez adaptery Wbudowana
Polimorfizm Ograniczony Elastycznie konfigurowalny

8. Typowe błędy podczas pracy z Gson

Błąd nr 1: Nie używasz TypeToken do kolekcji.
Jeśli deserializujesz listę lub mapę, koniecznie używaj TypeToken<>, w przeciwnym razie pojawią się dziwne błędy albo puste kolekcje.

Błąd nr 2: Brak konstruktora bezparametrowego.
Gson potrafi działać także bez konstruktora domyślnego, ale czasem przy deserializacji złożonych obiektów bez takiego konstruktora mogą wystąpić błędy. Lepiej zawsze dodać konstruktor bez parametrów, jeśli planujesz deserializację.

Błąd nr 3: Niezgodność nazw pól.
Jeśli w JSON pole nazywa się "full_name", a w klasie — "name", bez adnotacji @SerializedName("full_name") pole nie zostanie powiązane i wartość będzie null.

Błąd nr 4: Problemy z polami prywatnymi.
Gson potrafi serializować prywatne pola, ale jeśli są tylko prywatne pola i brak getterów/setterów, czasem pojawiają się problemy przy deserializacji. Lepiej używać getterów i setterów.

Błąd nr 5: Praca z datami.
Domyślnie Gson serializuje Date w mało wygodnym formacie. Dla LocalDate, LocalDateTime i innych nowych typów bez niestandardowych adapterów wystąpią błędy serializacji.

Błąd nr 6: Nie używasz @Expose, ale włączyłeś excludeFieldsWithoutExposeAnnotation().
Jeśli włączyłeś excludeFieldsWithoutExposeAnnotation(), ale nie oznaczyłeś pól adnotacją @Expose, nie będą one serializowane ani deserializowane — rezultat to pusty JSON albo obiekty z null.

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