JSON Toolkit

JSON na třídu Java: generátor dá kostru, pro produkci chybí ještě pět věcí

Shrnutí: Praktický pohled na to, co generátor JSON-to-Java skutečně emituje — čísla se stanou Double, vnořené objekty statickými vnitřními třídami a nevyprodukuje se jediná Jacksonova anotace. Peněžní přesnost, přejmenování polí, smazání typů, neznámá pole a data jsou pět věcí, které musíte doplnit sami, každý s kódem připraveným ke zkopírování.

Středně pokročilý~10 min čteníPublikováno 2026-11-04Aktualizováno 2026-11-04
Konverzní nástroj přeměňující JSON na kód třídy Java POJO

Ve zkratce: výstup generátoru se okamžitě zkompiluje, ale není připraven na produkci. Změřené výchozí nastavení našeho generátoru mapuje každé JSON číslo na Double, vnořené objekty mění na statické vnitřní třídy a nevygeneruje ani jednu Jacksonovu anotaci. Peněžní přesnost, přejmenování polí, smazání typů, neznámá pole a data — pět věcí, které nikdy neudělá za vás.

Přeměnit odpověď API na třídy Java je mechanická práce: přečíst JSON, opsat pole, obalit je gettermi a settermi. Cena mechanické práce je ten neviditelný nůž — špatně opsaný název vlastnosti nezpůsobí chybu kompilace. Pole tiše přijde jako null a odhalíte to až v produkčním logu. Generování struktury tříd existuje proto, aby názvy a typy byly sladěny od prvního dne. Ale generátor dokáže sladit jen to, co je ve vzorku vidět.

1. Nejdřív spusťte generátor: co vám vlastně dá

Vložte reprezentativní odpověď do našeho nástroje 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"
}

Vygenerovaná kostra (výchozí volby, useLombok vypnuto):

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; }
    // ...zbývající gettery/settery následují stejný vzor
}

Buyer a Item jsou samostatné statické vnitřní třídy. Pravidla mapování, jedno po druhém:

Podoba v JSON Vygenerovaný typ Java Poznámka
"name": "王五" String Řetězce jsou vždy String
"qty": 2 Double Každé číslo se stane Double, viz sekce 2
"vip": true Boolean Obálkový typ, null povoleno
"buyer": {...} Buyer (statická vnitřní třída) Vnořené objekty se stanou vlastní třídou
"items": [...] List<Item> Typ prvku převzat z prvního prvku
order_id orderId snake_case převeden na camelCase

Názvy tříd dostanou PascalCase (Root), pole camelCase (orderId) a kolize s klíčovými slovy Java (class, new, int) dostanou automaticky příponu. Existují tři volby: packageName, className a useLombok, která přepíná mezi @Data a ručně psanými gettery/settery. Se zapnutým Lombokem se celý blok boilerplate kódu smrskne na pár anotací.

Dvě hranice této kostry stojí za to znát předem. Za prvé, negeneruje žádné anotace Jackson/Gson — žádné @JsonProperty, žádné @JsonIgnoreProperties, žádné @JsonFormat, protože anotace rozhodují o tom, jak se deserializace provede, a vzorový payload nemůže odhalit vaši politiku provádění. Za druhé, neexistuje zpětná konverze: odvození JSON z třídy Java je mimo rozsah, stejný jednosměrný design jako cesta JSON to TypeScript.

2. Oprava číslo jedna: Double není typ na peníze

Generátor mapuje "price": 19.99 na Double. Zkompiluje se, přiřadí se, a přesto je to špatný typ. Binární pohyblivá řádová čárka nedokáže reprezentovat 0.1, což znamená, že nedokáže reprezentovat většinu desetinných zlomků:

System.out.println(19.99);          // 19.99 — vypadá v pořádku
double a = 0.1 + 0.2;
System.out.println(a);              // 0.30000000000000004
System.out.println(a == 0.3);       // false

Samotný tisk 19.99 vypadá čistě, protože Java aplikuje zkrášlení na nejkratší reprezentaci; chyba se projeví v okamžiku, kdy hodnota vstoupí do aritmetiky. Všechny tři technické texty na toto téma se shodují: ceny, částky a sazby by měly být BigDecimal, konstruované z řetězce — new BigDecimal("19.99"), nikoli new BigDecimal(19.99), který vnese chybu double přímo dovnitř.

Mechanická oprava je průchod po generování: nahradit Double za BigDecimal pouze u peněžních polí. Související past se stejným kořenem: celá čísla nad 2^53 (čísla objednávek, snowflake ID) vedená přes Double tiše ztrácejí koncové číslice — že dvaceticiferné číslo ztratí poslední tři číslice je naměřený, reprodukovatelný výsledek a mechanismus je stejný jako u toho, proč JSON ztrácí přesnost u velkých celých čísel. Ta pole namapujte na String nebo Long a nechte číslice cestovat jako text.

Skutečný význam pole Generátor dá Změnit na
Peníze, sazby Double BigDecimal (řetězcový konstruktor)
Čísla objednávek, ID (mohou přesáhnout 2^53) Double String nebo Long
Počty, čísla stránek Double Integer / Long
Skutečně vědecké floate Double ponechat

3. Oprava číslo dvě: neodpovídající názvy, přichycené přes @JsonProperty

Převést order_id na orderId je dobrá míněná domněnka: generátor předpokládá, že chcete na straně Java camelCase. Jackson však ve výchozím stavu porovnává JSON klíče s názvy polí doslova. Když si neodpovídají, nic neselže — orderId prostě zůstane null. Dvě řešení:

Na úrovni pole — přidejte @JsonProperty, jedna anotace přichytí jedno mapování:

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

Globálně — vyměňte naming strategii ObjectMapperu a v celém projektu nejsou potřeba žádné anotace po polích:

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

Volbu odvoďte od plochy: jediné fixní API — použijte úroveň pole; celý tým mluví se službami v snake_case — použijte globální strategii. Jeden negativní fakt si zaslouží vlastní odstavec — špatně napsaný název selže jako tiché null, ne jako výjimka. Stránka nástroje jsonformater považuje právě toto za důvod existence generátorů: mechanické opsání je přesně tam, kde tato chybovost vrcholí. Takže po přidání anotací napište alespoň jeden asertační test, že klíčová pole nejsou null.

4. Oprava číslo tři: smazání typů — nikdy nedeserializujte do holé List

Generátor vám předá deklaraci pole List<Item>, ale tím, co tuto seznam skutečně naplní, je Jackson. Deserializace na nejvyšší úrovni takto spadne přímo do pasti smazání typů:

// Špatně: prvky jsou za běhu LinkedHashMap
List<Item> items = mapper.readValue(json, List.class);

// Správně: TypeReference zachová generický parametr
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

readValue(json, List.class) se zkompiluje a spustí, ale Java generika existují jen v době kompilace. Za běhu Jackson vidí jen List, takže prvky přistanou jako LinkedHashMap. Chyba nevyskočí hned — vyskočí u prvního item.getSku() kdesi jinde, s ClassCastException daleko od místa, kde byla chyba spáchána. Tři správné formy podle scénáře:

// 1. Nejvyšší úrovní je pole
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

// 2. Pole objektu nejvyšší úrovně
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});

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

Na straně Gson je ekvivalentem TypeToken. Všechny tři průvodce uvádějí tuto položku samostatně, protože selhává pozdě a hlásí daleko.

5. Oprava číslo čtyři: neznámá pole — nenechte evoluci API rozbít deserializaci

To, že API přidá pole, je nejobyčejnější druh evoluce. Výchozí chování Jacksonu je však vyhodit UnrecognizedPropertyException u neznámých polí — dodavatel přidá pole a vaše služba začne chybovat. Vypněte to na dvou úrovních:

// Úroveň třídy: tolerovat neznámé jen u této třídy
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }

// Globálně: celý mapper je toleruje
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Jakmile to vypnete, objeví se zrcadlový problém: tolerance také ztlumí špatně napsané názvy polí a nechá volně působit režim selhání „tiché null“ z předchozí sekce. Kompromis je provozovat produkci s vypnutou tolerancí a testovací sadu s ní zapnutou, aby se pravopisné chyby odhalily při testování. Gson neznámá pole ignoruje ve výchozím stavu — proto působí bez námahy v rychlých integracích, a cena toho pohodlí je, že nikdy nevíte, co dodavatel přidal.

6. Oprava číslo pět: data a konstruktor bez argumentů

Generátor mapuje created_at na String, což je ve skutečnosti nejbezpečnější výchozí volba: v JSON je datum jen řetězec a formát na drátě může být ISO-8601, Unix timestamp nebo něco vlastního — vzorek vám nemůže říct, který chcete. Když potřebujete skutečný datový typ, přidejte dva řádky:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// Při serializaci vypisuje ISO-8601 místo polí timestampů
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;
}

Konstruktor bez argumentů je další povinná kontrola: Jackson deserializuje vytvořením objektu skrze něj a pak nastavuje pole jedno po druhém. Napíšete konstruktor s parametry a ten výchozí zmizí — deserializace okamžitě selže. S Lombokem přidejte @NoArgsConstructor; u record v Java 17 přenechte celý problém podpoře record v Jackson 2.12+, která páruje podle parametrů konstruktoru a požadavek zcela ruší.

7. Dvě produkční záležitosti: sdílejte ObjectMapper a validujte vstupy

Generátor vám dává třídy; dvě architektonické položky zůstávají za běhu. Za prvé, ObjectMapper je drahý na vytvoření a je thread-safe — sdílejte jednu instanci v celé aplikaci. Volat new ObjectMapper() uvnitř metody je nejčastější výkonnostní antipattern a instance, kterou Spring Boot standardně injektuje, existuje právě proto. Naming strategie, JavaTimeModule i politika neznámých polí patří na tu jedinou instanci, ne rozházené po volacích místech.

Za druhé, úspěšná deserializace říká, že typy souhlasí, ne že hodnoty jsou legální. Záporná cena, nulové množství, deformovaný e-mail — Jacksonovi je to jedno. Validaci vstupů přenechte Bean Validation (hibernate-validator), přidané jako anotace na vygenerovanou kostru:

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

    @Min(1)
    private Integer qty;

    @Email
    private String contact;
}

V Springu @Valid zachytí požadavek dřív, než dosáhne business logiky. Tato vrstva je něco, co generátor nikdy neposkytne — dokáže ze vzorku přečíst, jak pole vypadá, ale ne jaké byznys omezení pole nese.

8. Kde končí generování a začíná ruční psaní

Kdy generovat a kdy psát ručně je jasná linie:

  • Použijte generátor: odpovědi s mnoha poli, jednorázové integrace třetích stran, rychlé prototypy. Eliminuje právě jednu třídu chyb — mechanické chyby při opsávání — a za pár sekund vám předá kostru se sladěnými názvy a typy.
  • Pište ručně nebo přestavujte: peníze a dlouhá celá čísla potřebují změnu typu, naming strategie vyžaduje globální rozhodnutí, klíčové doménové objekty potřebují validační anotace. Tohle jsou byznys rozhodnutí, která vzorový payload nemůže udělat.
  • Společný předpoklad pro obojí: nejdřív ověřte, že zdrojový JSON je platný. Generátor rád vyprodukuje třídy z rozbitého payloadu a past nechá na deserializaci; před vložením jej projeďte naší validační stránkou. Deset sekund, tak či tak.

Generátor pokrývá „od nuly ke kostře“. Pět oprav v tomto článku pokrývá „od kostry k produkci“. Pořadí nelze obrátit a ani jeden krok nelze přeskočit.

FAQ

Lze vygenerované třídy použít přímo v Spring Boot?

Při kompilaci ano. Před během stojí za to udělat tři doplňky: peněžní pole na BigDecimal, naming strategii nebo @JsonProperty pro snake_case API a @JsonIgnoreProperties(ignoreUnknown = true) k přežití evoluce API. Anotace Bean Validation řiďte podle byznys pravidel.

Proč generátor mapuje každé číslo na Double místo long nebo BigDecimal?

Vzorek rozezná „celé vs. desetinné“, ale ne byznys význam. 2 může být množství i ID; dvacetimístné číslo objednávky i ¥19.99 jsou v JSON prostě čísla. Nechat sémantická rozhodnutí na uživateli je rozumná hranice pro dopředný generátor — cena je, že náhradní tabulku ze sekce 2 si musíte projít sami.

@Data z Lomboku nebo ručně psané gettery/settery — co vybrat?

Pokud tým už Lombok používá, vezměte @Data kvůli nejmenšímu množství boilerplate. Pokud se raději vyhýbáte pluginům v době kompilace nebo konfliktům s jinými annotation processory, pište je ručně. Jacksonovi je to jedno; dívá se na gettery/settery nebo viditelnost polí.

Vnořené objekty se stanou statickými vnitřními třídami — vadí to?

Jacksonovi ne; statická vnitřní třída se dá instanciovat nezávisle. Pro dlouhodobou údržbu je ale běžnější vytáhnout je jako třídy nejvyšší úrovně do balíčku kvůli znovupoužití a izolovanému testování. Struktura vnitřních tříd generátoru je stvořená k rychlému nahlédnutí, ne k živé codebase.

Pole List hází ClassCastException při deserializaci — jak to najdu?

Nejdřív zkontrolujte, zda deserializace nepoužila holou List.class: prvky jsou ve skutečnosti LinkedHashMap a chyba vyskočí při čtení pole. Přepněte na TypeReference. Pokud to stále selhává, vložte výjimku do naší stránky pro hledání chybových zpráv a následujte položku pro váš jazyk.