JSON Toolkit

JSON na klasę Java: generator da szkielet, do produkcji brakuje jeszcze pięciu rzeczy

Podsumowanie: Praktyczne spojrzenie na to, co generator JSON-to-Java naprawdę emituje — liczby stają się Double, zagnieżdżone obiekty statycznymi klasami wewnętrznymi, a nie powstaje ani jedna adnotacja Jacksona. Precyzja pieniędzy, zmiana nazw pól, wymazywanie typów, nieznane pola i daty to pięć rzeczy, które musisz uzupełnić sam, każda z kodem gotowym do wklejenia.

Średniozaawansowany~10 min czytaniaOpublikowano 2026-11-04Zaktualizowano 2026-11-04
Narzędzie konwertujące JSON na kod klasy POJO Java

W skrócie: wynik generatora od razu się kompiluje, ale nie jest gotowy do produkcji. Zmierzone domyślne ustawienia naszego generatora mapują każdą liczbę JSON na Double, zamieniają zagnieżdżone obiekty w statyczne klasy wewnętrzne i nie emitują żadnej adnotacji Jacksona. Precyzja pieniędzy, zmiana nazw pól, wymazywanie typów, nieznane pola i daty — pięć rzeczy, których nigdy nie zrobi za Ciebie.

Zamiana odpowiedzi API na klasy Java to praca mechaniczna: przeczytaj JSON, przepisz pola, opakuj je w gettery i settery. Koszt pracy mechanicznej to ten niewidzialny nóż — źle przepisana nazwa właściwości nie powoduje błędu kompilacji. Pole po cichu przychodzi jako null, a odkrywasz to w logu produkcyjnym. Generowanie struktury klas istnieje po to, by od pierwszego dnia trzymać nazwy i typy w ryzach. Ale generator może wyrównać tylko to, co jest widoczne w próbce.

1. Najpierw uruchom generator: co Ci faktycznie daje

Wklej reprezentatywną odpowiedź do naszego narzędzia JSON to Java:

{
  "order_id": 202611040001,
  "buyer": {
    "name": "王五",
    "vip": true
  },
  "items": [
    { "sku": "A-1", "price": 19.99, "qty": 2 }
  ],
  "created_at": "2026-11-04T09:30:00+08:00"
}

Wygenerowany szkielet (opcje domyślne, useLombok wyłączone):

public class Root {
    private Double orderId;
    private Buyer buyer;
    private List<Item> items;
    private String createdAt;

    public Double getOrderId() { return orderId; }
    public void setOrderId(Double orderId) { this.orderId = orderId; }
    // ...pozostałe gettery/settery idą tym samym schematem
}

Buyer i Item to osobne statyczne klasy wewnętrzne. Reguły mapowania, jedna po drugiej:

Kształt w JSON Wygenerowany typ Java Uwagi
"name": "王五" String Stringi zawsze stają się String
"qty": 2 Double Każda liczba staje się Double, zobacz sekcję 2
"vip": true Boolean Typ opakowujący, null dozwolone
"buyer": {...} Buyer (statyczna klasa wewnętrzna) Zagnieżdżone obiekty stają się osobną klasą
"items": [...] List<Item> Typ elementu brany z pierwszego elementu
order_id orderId snake_case zamieniony na camelCase

Nazwy klas dostają PascalCase (Root), pola camelCase (orderId), a kolizje ze słowami kluczowymi Java (class, new, int) dostają automatycznie sufiks. Istnieją trzy opcje: packageName, className i useLombok, przełączająca między @Data a ręcznie pisanymi getterami/setterami. Z włączonym Lombokiem cały blok boilerplate'u kurczy się do kilku adnotacji.

Dwie granice tego szkieletu warto znać z wyprzedzeniem. Po pierwsze, nie generuje żadnych adnotacji Jackson/Gson — żadnego @JsonProperty, żadnego @JsonIgnoreProperties, żadnego @JsonFormat, bo adnotacje decydują o tym, jak deserializacja się wykonuje, a przykładowy payload nie może ujawnić Twojej polityki wykonania. Po drugie, nie ma konwersji odwrotnej: wyprowadzanie JSON z klasy Java jest poza zakresem, ten sam jednokierunkowy projekt co ścieżka JSON to TypeScript.

2. Poprawka numer jeden: Double to nie jest typ na pieniądze

Generator mapuje "price": 19.99 na Double. Kompiluje się, przypisuje, a mimo to jest to zły typ. Binarny zmiennoprzecinkowy zapis nie potrafi reprezentować 0.1, co znaczy, że nie potrafi reprezentować większości ułamków dziesiętnych:

System.out.println(19.99);          // 19.99 — wygląda dobrze
double a = 0.1 + 0.2;
System.out.println(a);              // 0.30000000000000004
System.out.println(a == 0.3);       // false

Samo wypisanie 19.99 wygląda czysto, bo Java stosuje upiększanie do najkrótszej reprezentacji; błąd wychodzi w chwili, gdy wartość wchodzi w arytmetykę. Wszystkie trzy teksty inżynierskie na ten temat są zgodne: ceny, kwoty i stawki powinny być BigDecimal, konstruowane ze stringa — new BigDecimal("19.99"), a nie new BigDecimal(19.99), który wnosi błąd double'a prosto do środka.

Mechaniczna poprawka to przejście po generowaniu: podmień Double na BigDecimal tylko w polach pieniężnych. Powiązana pułapka z tym samym korzeniem: liczby całkowite ponad 2^53 (numery zamówień, ID snowflake) prowadzone przez Double po cichu tracą końcowe cyfry — dwudziestocyfrowa liczba tracąca ostatnie trzy cyfry to zmierzony, powtarzalny wynik, a mechanizm jest ten sam co u tego, dlaczego JSON traci precyzję na dużych liczbach całkowitych. Mapuj te pola na String lub Long i pozwól cyfrom podróżować jako tekst.

Prawdziwe znaczenie pola Generator daje Zmień na
Pieniądze, stawki Double BigDecimal (konstruktor ze stringa)
Numery zamówień, ID (mogą przekroczyć 2^53) Double String albo Long
Liczby sztuk, numery stron Double Integer / Long
Naprawdę naukowe floate Double zostaw

3. Poprawka numer dwa: niedopasowane nazwy, przypięte przez @JsonProperty

Zamiana order_id na orderId to dobrze intencjonowany zgadywacz: generator zakłada, że po stronie Java chcesz camelCase. Jackson jednak domyślnie porównuje klucze JSON z nazwami pól dosłownie. Gdy się nie zgadzają, nic nie pada — orderId po prostu zostaje nullem. Dwa remedia:

Poziom pola — dodaj @JsonProperty, jedna adnotacja przybija jedno mapowanie:

public class Root {
    @JsonProperty("order_id")
    private Long orderId;
}

Globalnie — podmień naming strategię ObjectMappera i w całym projekcie nie trzeba adnotacji przy każdym polu:

ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

Wybór zależy od powierzchni: jedno stałe API — poziom pola; cały zespół rozmawiający z serwisami w snake_case — strategia globalna. Jeden negatywny fakt zasługuje tu na osobny akapit — źle napisana nazwa zawodzi jako cichy null, a nie jako wyjątek. Strona narzędzia jsonformater uznaje to właśnie za powód istnienia generatorów: mechaniczne przepisywanie to dokładnie tam, gdzie ta częstotliwość błędów szczytuje. Więc po dodaniu adnotacji napisz przynajmniej jeden test asercji, że kluczowe pola nie są null.

4. Poprawka numer trzy: wymazywanie typów — nigdy nie deserializuj do nagiej List

Generator wręcza Ci deklarację pola List<Item>, ale tym, co naprawdę wypełnia tę listę, jest Jackson. Deserializacja na najwyższym poziomie tak spada prosto w pułapkę wymazywania typów:

// Źle: elementy są w czasie działania LinkedHashMap
List<Item> items = mapper.readValue(json, List.class);

// Dobrze: TypeReference zachowuje parametr generyczny
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

readValue(json, List.class) się kompiluje i działa, ale generyki Java istnieją tylko w czasie kompilacji. W czasie działania Jackson widzi tylko List, więc elementy lądują jako LinkedHashMap. Błąd nie wybucha od razu — wybucha przy pierwszym item.getSku() gdzieś tam, rzucając ClassCastException daleko od miejsca, gdzie błąd popełniono. Trzy poprawne formy, wedle scenariusza:

// 1. Najwyższy poziom to tablica
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

// 2. Pole obiektu najwyższego poziomu
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});

// 3. Jawna konstrukcja JavaType
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);

Po stronie Gson odpowiednikiem jest TypeToken. Wszystkie trzy przewodniki wymieniają to jako osobną pozycję, bo zawodzi późno i zgłasza daleko.

5. Poprawka numer cztery: nieznane pola — nie pozwól, by ewolucja API popsuła deserializację

To, że API dodaje pole, to najbardziej zwyczajna ewolucja. Domyślne zachowanie Jacksona to jednak rzucanie UnrecognizedPropertyException na nieznane pola — dostawca doda pole, a Twój serwis zaczyna się walić. Wyłącz to na dwóch poziomach:

// Poziom klasy: toleruj nieznane tylko w tej klasie
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }

// Globalnie: cały mapper je toleruje
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Gdy to wyłączysz, pojawia się lustrzany problem: tolerancja wycisza też źle wpisane nazwy pól, otwierając wrota trybowi awarii „cichy null” z poprzedniej sekcji. Droga środka to produkcja z wyłączoną tolerancją i zestaw testów z nią włączonym, by literówki wychodziły przy testowaniu. Gson domyślnie ignoruje nieznane pola — dlatego sprawdzi się bez wysiłku przy szybkich integracjach, a ceną tej wygody jest to, że nigdy nie wiesz, co dostawca dodał.

6. Poprawka numer pięć: daty i konstruktor bezargumentowy

Generator mapuje created_at na String, co jest zresztą najbezpieczniejszym domyślnym wyborem: w JSON data to po prostu string, a format na przewodzie może być ISO-8601, timestamp Uniksa albo coś własnego — próbka nie może Ci powiedzieć, który chcesz. Gdy potrzebujesz prawdziwego typu datowego, dodaj dwie linie:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// Przy serializacji emituj ISO-8601 zamiast tablic timestampów
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;
}

Konstruktor bezargumentowy to druga obowiązkowa kontrola: Jackson deserializuje przez utworzenie obiektu jego pośrednictwem, po czym ustawia pola jedno po drugim. Napiszesz konstruktor z parametrami, a domyślny znika — deserializacja pada od razu. Z Lombokiem dodaj @NoArgsConstructor; przy record w Java 17 oddaj cały problem wsparciu recordów w Jackson 2.12+, które paruje po parametrach konstruktora i usuwa ten wymóg w całości.

7. Dwie produkcyjne sprawy: współdziel ObjectMapper i waliduj wejście

Generator daje Ci klasy; dwa punkty architektoniczne zostają w czasie działania. Po pierwsze, ObjectMapper jest kosztowny w tworzeniu i jest thread-safe — współdziel jedną instancję w całej aplikacji. Wywoływanie new ObjectMapper() wewnątrz metody to najczęstszy antywzorzec wydajnościowy, a instancja wstrzykiwana domyślnie przez Spring Boot istnieje właśnie po to. Naming strategia, JavaTimeModule i polityka nieznanych pól należą do tej jednej instancji, nie rozsiane po miejscach wywołań.

Po drugie, udana deserializacja mówi, że typy się zgadzają, a nie że wartości są legalne. Ujemna cena, zerowa ilość, zdeformowany e-mail — Jacksonowi to obojętne. Walidacja wejścia należy do Bean Validation (hibernate-validator), doklejona jako adnotacje na wygenerowanym szkielecie:

public class CreateOrderRequest {
    @NotNull
    @DecimalMin(value = "0.01")
    private BigDecimal price;

    @Min(1)
    private Integer qty;

    @Email
    private String contact;
}

W Springu @Valid przechwytuje żądanie, zanim dotrze do logiki biznesowej. Ta warstwa jest czymś, czego generator nigdy nie dostarczy — potrafi z próbki odczytać, jak pole wygląda, ale nie jakie wiązanie biznesowe pole niesie.

8. Gdzie kończy się generowanie, a zaczyna pisanie ręczne

Kiedy generować, a kiedy pisać ręcznie, to jasna linia:

  • Użyj generatora: odpowiedzi z wieloma polami, jednorazowe integracje zewnętrzne, szybkie prototypy. Eliminuje dokładnie jedną klasę błędów — mechaniczne pomyłki przy przepisywaniu — i w kilka sekund wręcza szkielet z wyrównanymi nazwami i typami.
  • Pisz ręcznie lub przerabiaj: pieniądze i długie liczby całkowite wymagają zmiany typu, naming strategia wymaga decyzji globalnej, kluczowe obiekty domenowe wymagają adnotacji walidacyjnych. To są decyzje biznesowe, których przykładowy payload nie podejmie.
  • Wspólny warunek wstępny dla obu: najpierw potwierdź, że źródłowy JSON jest poprawny. Generator chętnie wyprodukuje klasy z popsutego payloadu i zostawi pułapkę na deserializację; przepuść go najpierw przez naszą stronę walidacji. Dziesięć sekund, tak czy owak.

Generator pokrywa „od zera do szkieletu”. Pięć poprawek z tego artykułu pokrywa „od szkieletu do produkcji”. Kolejności nie da się odwrócić i nie da się pominąć żadnego kroku.

FAQ

Czy wygenerowane klasy można użyć bezpośrednio w Spring Boot?

Na etapie kompilacji tak. Przed uruchomieniem warto zrobić trzy uzupełnienia: pola pieniężne na BigDecimal, naming strategię lub @JsonProperty dla API w snake_case i @JsonIgnoreProperties(ignoreUnknown = true) na przetrwanie ewolucji API. Adnotacje Bean Validation zgodnie z Twoimi regułami biznesowymi.

Dlaczego generator mapuje każdą liczbę na Double, a nie long lub BigDecimal?

Próbka odróżnia „całkowite vs. dziesiętne”, ale nie znaczenie biznesowe. 2 może być ilością albo ID; dwudziestocyfrowy numer zamówienia i ¥19.99 to w JSON po prostu liczby. Pozostawienie decyzji semantycznych użytkownikowi to rozsądna granica dla generatora forward — kosztem jest to, że tabelę podstawień z sekcji 2 musisz przejść sam.

@Data z Lomboka czy ręcznie pisane gettery/settery — które?

Jeśli zespół już używa Lomboka, weź @Data dla najmniej boilerplate'u. Jeśli wolisz uniknąć wtyczki w czasie kompilacji albo konfliktów z innymi procesorami adnotacji, pisz ręcznie. Jacksonowi wszystko jedno; patrzy na gettery/settery albo widoczność pól.

Zagnieżdżone obiekty stają się statycznymi klasami wewnętrznymi — czy to ma znaczenie?

Dla Jacksona nie; statyczna klasa wewnętrzna może być instancjonowana niezależnie. Do długoterminowego utrzymania większość zespołów wyciąga je jako klasy najwyższego poziomu do pakietu, dla ponownego użycia i izolowanego testowania. Struktura klas wewnętrznych generatora jest zbudowana do szybkiego podglądu, nie do żywej codebase'u.

Pole List rzuca ClassCastException podczas deserializacji — jak to namierzyć?

Najpierw sprawdź, czy deserializacja nie użyła nagiej List.class: elementy są w rzeczywistości LinkedHashMap i błąd wybucha przy odczycie pola. Przełącz się na TypeReference. Jeśli nadal pada, wklej wyjątek na naszą stronę wyszukiwania komunikatów o błędach i idź za wpisem dla Twojego języka.