JSON kepada Kelas Java: penjana hanya beri rangka, untuk produksi perlu lima perkara lagi
Inti pati: Tinjauan praktikal tentang apa yang penjana JSON-to-Java sebenarnya keluarkan — nombor jadi Double, objek bersarang jadi kelas dalaman statik, dan tiada anotasi Jackson langsung dihasilkan. Ketepatan wang, penamaan semula medan, pemadaman jenis, medan tidak dikenali dan tarikh ialah lima perkara yang anda sendiri perlu tambah, setiap satu dengan kod sedia salin.

Ringkasnya: output penjana terus boleh disusun tetapi tidak bersedia untuk produksi. Nilai lalai terukur penjana kami memetakan setiap nombor JSON kepada Double, mengubah objek bersarang kepada kelas dalaman statik, dan tidak mengeluarkan sebarang anotasi Jackson. Ketepatan wang, penamaan semula medan, pemadaman jenis, medan tidak dikenali dan tarikh — lima perkara yang tidak akan dilakukannya untuk anda.
Mengubah respons API kepada kelas Java ialah kerja mekanikal: baca JSON, salin medan, bungkus dalam getter dan setter. Harga kerja mekanikal ialah pisau tak nampak itu — nama properti tersalin salah satu huruf tidak menghasilkan ralat susunan. Medan sampai secara senyap sebagai null, dan anda mendapatinya dalam log produksi. Penjanaan struktur kelas wujud untuk menjajarkan nama dan jenis dari hari pertama. Tetapi penjana hanya boleh menjajarkan apa yang kelihatan dalam sampel.
1. Jalankan penjana dahulu: apa sebenarnya yang diberikannya
Tampal respons yang mewakili ke dalam alat JSON to Java kami:
{
"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"
}
Rangka yang dijana (pilihan lalai, useLombok dimatikan):
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; }
// ...getter/setter yang selebihnya mengikut corak sama
}
Buyer dan Item ialah kelas dalaman statik berasingan. Peraturan pemetaan, satu demi satu:
| Bentuk dalam JSON | Jenis Java yang dijana | Nota |
|---|---|---|
"name": "王五" |
String |
Rentetan sentiasa jadi String |
"qty": 2 |
Double |
Setiap nombor jadi Double, lihat bahagian 2 |
"vip": true |
Boolean |
Jenis pembalut, null dibenarkan |
"buyer": {...} |
Buyer (kelas dalaman statik) |
Objek bersarang jadi kelas tersendiri |
"items": [...] |
List<Item> |
Jenis elemen diambil daripada elemen pertama |
order_id |
orderId |
snake_case ditukar kepada camelCase |
Nama kelas mengambil PascalCase (Root), medan mengambil camelCase (orderId), dan pertembungan dengan kata kunci Java (class, new, int) mendapat imbuhan akhir secara automatik. Ada tiga pilihan: packageName, className dan useLombok, yang bertukar antara @Data dan getter/setter tulisan tangan. Dengan Lombok aktif, keseluruhan blok kod berulang mengecil kepada beberapa anotasi.
Dua sempadan rangka ini patut diketahui lebih awal. Pertama, ia tidak menjana sebarang anotasi Jackson/Gson — tiada @JsonProperty, tiada @JsonIgnoreProperties, tiada @JsonFormat, kerana anotasi menentukan bagaimana deserialization dilaksanakan, dan muatan sampel tidak boleh mendedahkan polisi pelaksanaan anda. Kedua, tiada penukaran songsang: menerbitkan JSON daripada kelas Java di luar skop, reka bentuk sehala yang sama seperti laluan JSON to TypeScript.
2. Pembetulan pertama: Double bukan jenis untuk wang
Penjana memetakan "price": 19.99 kepada Double. Ia tersusun, ia ditugaskan, dan tetap jenis yang salah. Titik terapung binari tidak boleh mewakili 0.1, bermakna ia tidak boleh mewakili kebanyakan pecahan perpuluhan:
System.out.println(19.99); // 19.99 — nampak kemas
double a = 0.1 + 0.2;
System.out.println(a); // 0.30000000000000004
System.out.println(a == 0.3); // false
Mencetak 19.99 sahaja kelihatan bersih kerana Java menggunakan penghalusan perwakilan terpendek; ralat muncul begitu nilai masuk ke dalam aritmetik. Ketiga-tiga artikel kejuruteraan mengenai topik ini sepakat: harga, jumlah dan kadar patut menjadi BigDecimal, dibina daripada rentetan — new BigDecimal("19.99"), bukan new BigDecimal(19.99), yang membawa ralat double terus ke dalam.
Pembetulan mekanikalnya ialah laluan selepas penjanaan: ganti Double dengan BigDecimal hanya pada medan wang. Perangkap berkaitan pada akar sama: integer melebihi 2^53 (nombor pesanan, ID snowflake) yang melalui Double kehilangan digit akhir secara senyap — nombor 20 digit yang hilang tiga digit terakhirnya ialah hasil terukur yang boleh diulang, dan mekanismenya sama seperti di sebalik mengapa JSON hilang kejituan pada integer besar. Petakan medan itu kepada String atau Long dan biarkan digit bergerak sebagai teks.
| Maksud sebenar medan | Penjana beri | Tukar kepada |
|---|---|---|
| Wang, kadar | Double |
BigDecimal (pembina rentetan) |
| Nombor pesanan, ID (boleh melebihi 2^53) | Double |
String atau Long |
| Kiraan, nombor halaman | Double |
Integer / Long |
| Float sains sebenar | Double |
kekalkan |
3. Pembetulan kedua: nama tidak sepadan, pin dengan @JsonProperty
Menukar order_id kepada orderId ialah tekaan niat baik: penjana menganggap anda mahu camelCase di sisi Java. Tetapi Jackson secara lalai memadankan kunci JSON dengan nama medan secara literal. Apabila tidak sepadan, tiada apa yang gagal — orderId hanya kekal null. Dua jalan ubah:
Peringkat medan — tambah @JsonProperty, satu anotasi menyemat satu pemetaan:
public class Root {
@JsonProperty("order_id")
private Long orderId;
}
Global — tukar strategi penamaan ObjectMapper dan tiada anotasi per medan diperlukan di mana-mana dalam projek:
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
Pilihan mengikut luas sambungan: satu API tetap — gunakan peringkat medan; seluruh pasukan bercakap dengan perkhidmatan snake_case — gunakan strategi global. Satu fakta negatif layak mendapat perenggan sendiri di sini — nama tersalah tulis gagal sebagai null senyap, bukan sebagai pengecualian. Halaman alat jsonformater menganggap inilah sebab penjana wujud: penyalinan mekanikal ialah tepat tempat kadar ralat ini memuncak. Jadi selepas menambah anotasi, tulis sekurang-kurangnya satu ujian asersi bahawa medan kunci bukan null.
4. Pembetulan ketiga: pemadaman jenis — jangan sesekali deserialise ke dalam List kosong
Penjana menyerahkan perisytiharan medan List<Item> kepada anda, tetapi benda yang benar-benar mengisi senarai itu ialah Jackson. Deserialise pada peringkat atas begini terus terjerat dalam pemadaman jenis:
// Salah: elemen semasa masa larian ialah LinkedHashMap
List<Item> items = mapper.readValue(json, List.class);
// Betul: TypeReference mengekalkan parameter generik
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});
readValue(json, List.class) tersusun dan berjalan, tetapi generik Java hanya wujud semasa susunan. Semasa larian Jackson hanya melihat List, jadi elemen mendarat sebagai LinkedHashMap. Ralat tidak meletup serta-merta — ia meletup di item.getSku() pertama di suatu tempat, membaling ClassCastException jauh dari tempat kesilapan dilakukan. Tiga bentuk betul, mengikut senario:
// 1. Peringkat atas ialah tatasusunan itu sendiri
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});
// 2. Medan objek peringkat atas
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});
// 3. Pembinaan JavaType secara eksplisit
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);
Di pihak Gson, padanannya ialah TypeToken. Ketiga-tiga panduan menyenaraikannya sebagai butiran tersendiri, kerana ia gagal lewat dan melaporkan jauh.
5. Pembetulan keempat: medan tidak dikenali — jangan biarkan evolusi API merosakkan deserialization
API menambah medan ialah jenis evolusi paling biasa. Tetapi kelakuan lalai Jackson ialah membaling UnrecognizedPropertyException pada medan tidak dikenali — pembekal tambah satu medan dan perkhidmatan anda mula berkeleyar. Matikan pada dua peringkat:
// Peringkat kelas: hanya pada kelas ini medan tidak dikenali ditoleransi
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }
// Global: keseluruhan mapper menoleransinya
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Selepas dimatikan, masalah cermin muncul: toleransi turut membisukan nama medan yang tersalah eja, membuka pintu lebar-lebar kepada mod kegagalan "null senyap" bahagian sebelumnya. Jalan tengahnya: jalankan produksi dengan toleransi dimatikan dan suite ujian dengannya dihidupkan, supaya kesilapan ejaan muncul semasa pengujian. Gson secara lalai mengabaikan medan tidak dikenali — sebab itu ia terasa senang untuk integrasi pantas, dan harga keselesaan itu ialah anda tidak pernah tahu apa yang pembekal tambahkan.
6. Pembetulan kelima: tarikh dan pembina tanpa hujah
Penjana memetakan created_at kepada String, yang sebenarnya lalai paling selamat: dalam JSON, tarikh cuma rentetan, dan format atas talian boleh jadi ISO-8601, cap masa Unix, atau tersuai — sampel tidak boleh memberitahu anda yang mana anda mahu. Apabila jenis tarikh sebenar diperlukan, tambah dua baris:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// Keluarkan ISO-8601 semasa pensirian dan bukan tatasusunan cap masa
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
}
Pembina tanpa hujah ialah semakan wajib yang lain: Jackson mendeserialisasi dengan mencipta objek menerusinya, kemudian menetapkan medan satu demi satu. Anda tulis pembina berparameter, pembina lalai hilang — deserialization gagal serta-merta. Dengan Lombok, tambah @NoArgsConstructor; dengan record Java 17, serahkan keseluruhan masalah kepada sokongan record Jackson 2.12+, yang memadankan melalui parameter pembina dan memansuhkan syarat ini sepenuhnya.
7. Dua perkara produksi: guna semula ObjectMapper dan sahkan input
Penjana memberi anda kelas; dua perkara peringkat seni bina kekal pada masa larian. Pertama, ObjectMapper mahal untuk dicipta dan selamat diguna serentak — kongsi satu contoh merentasi keseluruhan aplikasi. Memanggil new ObjectMapper() dalam kaedah ialah anti-corak prestasi paling lazim, dan contoh yang Spring Boot suntik secara lalai wujud tepat atas sebab itu. Strategi penamaan, JavaTimeModule dan polisi medan tidak dikenali semuanya patut berada pada contoh tunggal itu, bukan berselerak di tapak panggilan.
Kedua, deserialization yang berjaya bermakna jenis sepadan, bukan bermakna nilai sah. Harga negatif, kuantiti sifar, e-mel cacat — Jackson tidak ambil kisah. Pengesahan input menjadi tugas Bean Validation (hibernate-validator), ditambah sebagai anotasi pada rangka yang dijana:
public class CreateOrderRequest {
@NotNull
@DecimalMin(value = "0.01")
private BigDecimal price;
@Min(1)
private Integer qty;
@Email
private String contact;
}
Dalam Spring, @Valid menyekat permintaan sebelum ia sampai ke logik perniagaan. Lapisan ini sesuatu yang penjana tidak akan pernah dapat berikan — ia boleh membaca daripada sampel rupa sebuah medan, tetapi bukan kekangan perniagaan yang dibawa medan itu.
8. Di mana penjanaan tamat dan penulisan tangan bermula
Bila menjana dan bila menulis tangan ialah garisan jelas:
- Gunakan penjana: respons bermedan banyak, integrasi pihak ketiga sekali guna, prototaip pantas. Ia menghapuskan tepat satu kelas ralat — kesilapan salin mekanikal — dan menyerahkan rangka yang terjajarkan nama dan jenis dalam beberapa saat.
- Tulis tangan atau ubah suai mendalam: wang dan integer panjang perlu penukaran jenis, strategi penamaan perlu keputusan global, objek domain teras perlu anotasi pengesahan. Ini keputusan perniagaan yang tidak boleh dibuat oleh muatan sampel.
- Prasyarat bersama untuk kedua-duanya: sahkan dahulu sumber JSON itu sah. Penjana gembira menjana kelas daripada muatan rosak dan meninggalkan perangkap itu untuk masa deserialization; laluikannya dahulu melalui halaman pengesahan kami. Sepuluh saat, bagaimana pun juga.
Penjana meliputi "dari sifar ke rangka". Lima pembetulan dalam artikel ini meliputi "dari rangka ke produksi". Turutan tidak boleh dibalik, dan tiada langkah boleh dilangkau.
FAQ
Bolehkah kelas yang dijana digunakan terus dalam Spring Boot?
Semasa susunan, boleh. Sebelum larian, tiga tambahan patut dilakukan: medan wang kepada BigDecimal, strategi penamaan atau @JsonProperty untuk API snake_case, dan @JsonIgnoreProperties(ignoreUnknown = true) untuk bertahan daripada evolusi API. Anotasi Bean Validation mengikut peraturan perniagaan anda.
Mengapa penjana memetakan setiap nombor kepada Double dan bukannya long atau BigDecimal?
Sampel membezakan "integer lawan perpuluhan" tetapi bukan maksud perniagaan. 2 boleh jadi kuantiti atau ID; nombor pesanan 20 digit dan ¥19.99 kedua-duanya sekadar nombor dalam JSON. Membiarkan keputusan semantik kepada pengguna ialah sempadan munasabah bagi penjana ke hadapan — harganya ialah jadual penggantian bahagian 2 perlu anda lalui sendiri.
@Data Lombok atau getter/setter tulisan tangan — yang mana?
Jika pasukan sudah menggunakan Lombok, ambil @Data untuk kod berulang paling kurang. Jika anda lebih suka mengelakkan pemalam semasa susunan atau konflik dengan pemproses anotasi lain, tulis secara manual. Bagi Jackson tiada beza; ia memandang getter/setter atau keterlihatan medan.
Objek bersarang menjadi kelas dalaman statik — adakah penting?
Bukan pada Jackson; kelas dalaman statik boleh dibina contohnya secara bebas. Untuk penyelenggaraan jangka panjang, kebanyakan pasukan mengekstraknya sebagai kelas peringkat atas dalam pakej, untuk kegunaan semula dan pengujian berasingan. Susunan kelas dalaman penjana dibina untuk pemeriksaan pantas, bukan untuk codebase yang hidup.
Medan List membaling ClassCastException semasa deserialization — bagaimana mengesan?
Semak dahulu adakah deserialization menggunakan List.class kosong: elemen sebenarnya LinkedHashMap dan ralat meloncat semasa membaca medan. Tukar kepada TypeReference. Jika masih gagal, tampal pengecualian itu ke halaman carian mesej ralat kami dan ikuti kemasukan untuk bahasa anda.