JSON Toolkit

JSON ke Kelas Java: generator hanya memberi kerangka, untuk produksi masih kurang lima hal

Intinya: Tinjauan praktis tentang apa yang generator JSON-to-Java benar-benar keluarkan — angka menjadi Double, objek bersarang menjadi static inner class, dan tidak satu pun anotasi Jackson dihasilkan. Presisi uang, penggantian nama field, type erasure, field tak dikenal, dan tanggal adalah lima hal yang harus Anda tambahkan sendiri, masing-masing dengan kode siap salin.

Menengah±10 menit bacaDiterbitkan 2026-11-04Diperbarui 2026-11-04
Antarmuka alat konversi yang mengubah JSON menjadi kode kelas POJO Java

Ringkasnya: hasil generator langsung bisa dikompilasi tapi belum siap produksi. Nilai default yang terukur dari generator di situs ini memetakan setiap angka JSON ke Double, mengubah objek bersarang menjadi static inner class, dan tidak menghasilkan satu anotasi Jackson pun. Presisi uang, penggantian nama field, type erasure, field tak dikenal, dan tanggal — lima hal yang tidak akan pernah ia kerjakan untuk Anda.

Mengubah respons API menjadi kelas Java adalah kerja mekanis murni: baca JSON, salin field, bungkus dengan getter/setter. Harga kerja mekanis adalah pisau tak terlihat itu — nama properti tersalin salah satu huruf tidak memicu error kompilasi. Field datang senyap sebagai null, dan Anda menemukannya di log produksi. Pembuatan struktur kelas ada untuk menyelaraskan nama dan tipe sejak hari pertama. Tapi generator hanya bisa menyelaraskan apa yang terlihat di sampel.

1. Jalankan generator dulu: apa sebenarnya yang ia berikan

Tempelkan respons yang representatif ke alat JSON to Java di situs:

{
  "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"
}

Kerangka yang dihasilkan (opsi default, useLombok mati):

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 sisanya mengikuti pola yang sama
}

Buyer dan Item adalah static inner class terpisah. Aturan pemetaan, satu per satu:

Bentuk di JSON Tipe Java yang dihasilkan Catatan
"name": "王五" String String selalu menjadi String
"qty": 2 Double Semua angka menjadi Double, lihat bagian 2
"vip": true Boolean Tipe pembungkus, null diizinkan
"buyer": {...} Buyer (static inner class) Objek bersarang menjadi kelas sendiri
"items": [...] List<Item> Tipe elemen diambil dari elemen pertama
order_id orderId snake_case dikonversi ke camelCase

Nama kelas mendapat PascalCase (Root), field mendapat camelCase (orderId), dan tumbukan dengan kata kunci Java (class, new, int) otomatis diberi akhiran. Ada tiga opsi: packageName, className, dan useLombok, yang beralih antara @Data dan getter/setter tulisan tangan. Dengan Lombok aktif, seluruh blok kode berulang menyusut menjadi beberapa anotasi.

Dua batas kerangka ini layak diketahui di muka. Pertama, ia tidak menghasilkan anotasi Jackson/Gson sama sekali — tanpa @JsonProperty, tanpa @JsonIgnoreProperties, tanpa @JsonFormat, karena anotasi menentukan bagaimana deserialization dieksekusi, dan payload sampel tidak bisa membocorkan kebijakan eksekusi Anda. Kedua, tidak ada konversi terbalik: menganulir JSON dari kelas Java di luar jangkauan, desain satu arah yang sama dengan jalur JSON to TypeScript.

2. Perbaikan nomor satu: Double bukan tipe untuk uang

Generator memetakan "price": 19.99 ke Double. Bisa dikompilasi, bisa diisi, dan tetap tipe yang salah. Titik apung biner tidak bisa merepresentasikan 0.1, artinya tidak bisa merepresentasikan sebagian besar pecahan desimal:

System.out.println(19.99);          // 19.99 — terlihat wajar
double a = 0.1 + 0.2;
System.out.println(a);              // 0.30000000000000004
System.out.println(a == 0.3);       // false

Mencetak 19.99 sendirian tampak bersih karena Java menerapkan penghalusan representasi terpendek; kesalahannya muncul begitu nilai masuk ke aritmetika. Ketiga tulisan teknik tentang topik ini sepakat: harga, jumlah, dan tarif seharusnya BigDecimal, dibangun dari string — new BigDecimal("19.99"), bukan new BigDecimal(19.99), yang membawa error double masuk begitu saja.

Perbaikan mekanisnya adalah satu lintasan setelah pembuatan: ganti Double dengan BigDecimal hanya pada field uang. Jebakan terkait dengan akar sama: bilangan bulat di atas 2^53 (nomor pesanan, snowflake ID) yang lewat Double diam-diam kehilangan digit akhirnya — angka 20 digit yang kehilangan tiga digit terakhirnya adalah hasil yang terukur dan dapat direproduksi, dan mekanismenya sama persis dengan yang berada di balik mengapa JSON kehilangan presisi pada bilangan bulat besar. Petakan field itu ke String atau Long dan biarkan digit bepergian sebagai teks.

Makna asli field Generator memberi Ganti ke
Uang, tarif Double BigDecimal (konstruktor string)
Nomor pesanan, ID (bisa melewati 2^53) Double String atau Long
Hitungan, nomor halaman Double Integer / Long
Float ilmiah sungguhan Double biarkan

3. Perbaikan nomor dua: nama tak cocok, dikunci dengan @JsonProperty

Mengubah order_id menjadi orderId adalah tebakan niat baik: generator menganggap Anda menginginkan camelCase di sisi Java. Tapi Jackson secara default mencocokkan kunci JSON dengan nama field secara harfiah. Saat tidak cocok, tidak ada yang gagal — orderId hanya diam jadi null. Dua jalan perbaikan:

Per field — tambahkan @JsonProperty, satu anotasi mengunci satu pemetaan:

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

Global — ganti strategi penamaan ObjectMapper dan tidak perlu anotasi per field di mana pun di proyek:

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

Pilihan mengikuti luas permukaan: satu API tetap — pakai level field; satu tim penuh berbicara dengan layanan snake_case — pakai strategi global. Satu fakta negatif layak punya paragraf sendiri di sini — nama yang salah ketik gagal sebagai null senyap, bukan sebagai exception. Halaman alat jsonformater menganggap inilah alasan generator ada: penyalinan mekanis persis tempat tingkat kesalahan ini memuncak. Jadi setelah menambah anotasi, tulis setidaknya satu test assertion bahwa field kunci tidak null.

4. Perbaikan nomor tiga: type erasure — jangan pernah deserialisasi ke List telanjang

Generator menyerahkan deklarasi field List<Item> kepada Anda, tapi yang benar-benar mengisi daftar itu adalah Jackson. Mendeserialisasi di tingkat atas seperti ini jatuh langsung ke jebakan type erasure:

// Salah: elemennya adalah LinkedHashMap saat runtime
List<Item> items = mapper.readValue(json, List.class);

// Benar: TypeReference mempertahankan parameter generik
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

readValue(json, List.class) bisa dikompilasi dan berjalan, tapi generik Java hanya ada saat kompilasi. Saat runtime Jackson hanya melihat List, sehingga elemennya mendarat sebagai LinkedHashMap. Errornya tidak meledak langsung — meledak di item.getSku() pertama entah di mana, melempar ClassCastException jauh dari tempat kesalahan dibuat. Tiga bentuk yang benar, sesuai skenario:

// 1. Tingkat atasnya sendiri adalah array
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

// 2. Field dari objek tingkat atas
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});

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

Di sisi Gson padanannya TypeToken. Ketiga panduan mencantumkannya sebagai butir tersendiri, karena gagal belakangan dan melapor dari jauh.

5. Perbaikan nomor empat: field tak dikenal — jangan biarkan evolusi API merusak deserialization

API menambah satu field adalah bentuk evolusi paling biasa. Tapi perilaku default Jackson adalah melempar UnrecognizedPropertyException pada field tak dikenal — penyedia menambah satu field dan layanan Anda langsung mulai error. Matikan di dua tingkat:

// Level kelas: toleransi field tak dikenal hanya di kelas ini
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }

// Global: seluruh mapper menoleransinya
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Setelah dimatikan, muncul persoalan cermin: toleransi juga membisikan nama field yang salah ketik, membuka lebar-lebar mode kegagalan "null senyap" dari bagian sebelumnya. Jalan tengahnya: jalankan produksi dengan toleransi mati dan suite pengujian dengan toleransi nyala, agar salah ketik terbongkar saat pengujian. Gson mengabaikan field tak dikenal secara default — karena itu ia terasa enteng saat integrasi cepat, dan harga kenyamanan itu adalah Anda tak pernah tahu apa yang penyedia tambahkan.

6. Perbaikan nomor lima: tanggal dan konstruktor tanpa argumen

Generator memetakan created_at ke String, yang sebenarnya default paling aman: di JSON tanggal hanyalah string, dan formatnya di kabel bisa ISO-8601, timestamp Unix, atau kustom — sampel tidak bisa bilang Anda ingin yang mana. Saat butuh tipe tanggal sungguhan, tambahkan dua baris:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// Saat serialisasi keluarkan ISO-8601, bukan array timestamp
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;
}

Konstruktor tanpa argumen adalah pemeriksaan wajib lainnya: Jackson mendeserialisasi dengan menciptakan objek lewatnya, lalu mengeset field satu per satu. Anda menulis konstruktor berparameter, default-nya hilang — deserialization langsung gagal. Dengan Lombok, tambahkan @NoArgsConstructor; dengan record Java 17, serahkan seluruh persoalan ke dukungan record Jackson 2.12+ yang mencocokkan lewat parameter konstruktor dan menghapus syarat ini sepenuhnya.

7. Dua urusan produksi: pakai ulang ObjectMapper dan validasi input

Generator memberi Anda kelas; dua urusan arsitektur tersisa saat runtime. Pertama, membuat ObjectMapper mahal dan ia thread-safe — bagikan satu instance ke seluruh aplikasi. Memanggil new ObjectMapper() di dalam method adalah anti-pola performa paling umum, dan instance yang disuntikkan Spring Boot secara default ada persis karena itu. Strategi penamaan, JavaTimeModule, dan kebijakan field tak dikenal semuanya berada di satu instance itu, jangan tersebar di titik-titik panggilan.

Kedua, deserialization yang berhasil hanya berarti tipe cocok, bukan berarti nilai sah. Harga negatif, jumlah nol, email cacat — Jackson tidak peduli. Validasi input menjadi tugas Bean Validation (hibernate-validator), ditambahkan sebagai anotasi pada kerangka hasil generator:

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

    @Min(1)
    private Integer qty;

    @Email
    private String contact;
}

Di Spring, @Valid mencegat request sebelum masuk ke logika bisnis. Lapisan ini sesuatu yang tidak akan pernah bisa diberikan generator — ia bisa membaca dari sampel seperti apa bentuk sebuah field, tapi tidak bisa membaca batasan bisnis apa yang dibawa field itu.

8. Di mana generator berhenti dan tulisan tangan dimulai

Kapan membuat dengan generator dan kapan menulis tangan adalah garis yang jelas:

  • Pakai generator: respons berfield banyak, integrasi pihak ketiga sekali pakai, prototipe cepat. Ia menghapus tepat satu kelas kesalahan — kesalahan menyalin mekanis — dan menyerahkan kerangka yang nama dan tipenya selaras dalam beberapa detik.
  • Tulis tangan atau rombak mendalam: uang dan bilangan bulat panjang butuh perubahan tipe, strategi penamaan butuh keputusan global, objek domain inti butuh anotasi validasi. Ini keputusan bisnis yang tidak bisa diambil payload sampel.
  • Prasyarat bersama untuk keduanya: pastikan dulu JSON sumbernya valid. Generator dengan senang hati menghasilkan kelas dari payload rusak dan menyisakan jebakan saat deserialization; lewati dulu lewat halaman validasi situs sebelum menempel. Sepuluh detik, mana pun arahnya.

Generator menangani "dari nol ke kerangka". Lima perbaikan di artikel ini menangani "dari kerangka ke produksi". Urutannya tak bisa dibalik, dan tak satu langkah pun bisa dilewati.

FAQ

Apakah kelas yang dihasilkan bisa langsung dipakai di Spring Boot?

Saat kompilasi, bisa. Sebelum runtime tiga tambahan layak dilakukan: field uang ke BigDecimal, strategi penamaan atau @JsonProperty untuk API snake_case, dan @JsonIgnoreProperties(ignoreUnknown = true) untuk selamat dari evolusi API. Anotasi Bean Validation mengikuti aturan bisnis Anda.

Mengapa generator memetakan semua angka ke Double, bukan long atau BigDecimal?

Sampel bisa membedakan "bulat vs. desimal" tapi tidak makna bisnisnya. 2 bisa berupa kuantitas atau ID; nomor pesanan 20 digit dan ¥19.99 sama-sama sekadar number di JSON. Menyerahkan keputusan semantik ke pengguna adalah batas yang wajar untuk generator satu arah — harganya adalah tabel penggantian di bagian 2 harus Anda lewati sendiri.

@Data milik Lombok atau getter/setter tulisan tangan — yang mana?

Jika tim sudah memakai Lombok, ambil @Data untuk paling sedikit kode berulang. Jika lebih suka menghindari plugin waktu kompilasi atau konflik dengan annotation processor lain, tulis manual. Bagi Jackson tak ada bedanya; ia melihat getter/setter atau visibilitas field.

Objek bersarang menjadi static inner class — apakah berpengaruh?

Bagi Jackson tidak; static inner class bisa diinstansiasi secara independen. Namun untuk pemeliharaan jangka panjang, kebanyakan tim mengeluarkannya sebagai kelas tingkat atas di sebuah package, untuk pemakaian ulang dan pengujian terpisah. Struktur inner class milik generator dibuat untuk pemeriksaan cepat, bukan untuk codebase yang hidup.

Field List melempar ClassCastException saat deserialisasi — bagaimana melacaknya?

Cek dulu apakah deserialization memakai List.class telanjang: elemennya sebenarnya LinkedHashMap dan error meledak saat field dibaca. Beralihlah ke TypeReference. Jika tetap gagal, tempelkan exception ke halaman pencarian pesan error di situs dan ikuti entri untuk bahasa Anda.