JSON से Java क्लास: जनरेटर कंकाल देता है, प्रोडक्शन के लिए पाँच चीज़ें और जोड़नी होंगी
मुख्य बात: एक JSON-to-Java जनरेटर वास्तव में क्या आउटपुट देता है, इसका व्यावहारिक अवलोकन — संख्याएँ Double बन जाती हैं, नेस्टेड ऑब्जेक्ट static इनर क्लास बनते हैं, और एक भी Jackson एनोटेशन नहीं बनता। पैसे की सटीकता, फ़ील्ड रीनेमिंग, टाइप इरेज़र, अज्ञात फ़ील्ड और तारीखें — ये वह पाँच चीज़ें हैं जो आपको खुद जोड़नी होंगी, हर एक के लिए कॉपी-पेस्ट कोड के साथ।

संक्षेप में: जनरेटर का आउटपुट तुरंत कंपाइल हो जाता है पर प्रोडक्शन-रेडी नहीं होता। हमारे जनरेटर के मापे गए डिफ़ॉल्ट हर JSON संख्या को Double में मैप करते हैं, नेस्टेड ऑब्जेक्ट को static इनर क्लास बनाते हैं, और एक भी Jackson एनोटेशन नहीं देते। पैसे की सटीकता, फ़ील्ड नाम बदलना, टाइप इरेज़र, अज्ञात फ़ील्ड और तारीखें — पाँच चीज़ें जो वह कभी आपके लिए नहीं करेगा।
किसी API रिस्पॉन्स को Java क्लास में बदलना शुद्ध यांत्रिक काम है: JSON पढ़ो, फ़ील्ड उतारो, उन्हें getter/setter में लपेटो। यांत्रिक काम की कीमत वही अदृश्य चाकू है — प्रॉपर्टी का नाम एक अक्षर गलत उतारने पर कंपाइल एरर नहीं आता। फ़ील्ड चुपचाप null आता है, और आपको पता प्रोडक्शन लॉग में चलता है। क्लास स्ट्रक्चर जनरेट करने का मक़सद है पहले दिन से नाम और टाइप को संरेखित रखना। पर जनरेटर सिर्फ़ वही संरेखित कर सकता है जो सैंपल में दिखता है।
1. पहले जनरेटर चलाओ: वह असल में क्या देता है
एक प्रतिनिधि रिस्पॉन्स हमारे 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"
}
जनरेट हुआ कंकाल (डिफ़ॉल्ट विकल्प, useLombok बंद):
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 उसी पैटर्न में चलते हैं
}
Buyer और Item अलग static इनर क्लास हैं। मैपिंग नियम, एक-एक करके:
| JSON में रूप | जनरेट हुआ Java टाइप | नोट |
|---|---|---|
"name": "王五" |
String |
स्ट्रिंग हमेशा String बनती है |
"qty": 2 |
Double |
हर संख्या Double बनती है, सेक्शन 2 देखें |
"vip": true |
Boolean |
रैपर टाइप, null मान्य |
"buyer": {...} |
Buyer (static इनर क्लास) |
नेस्टेड ऑब्जेक्ट अपनी क्लास बनता है |
"items": [...] |
List<Item> |
एलिमेंट टाइप पहले एलिमेंट से लिया जाता है |
order_id |
orderId |
snake_case camelCase में बदला |
क्लास नाम PascalCase लेते हैं (Root), फ़ील्ड camelCase (orderId), और Java कीवर्ड से टकराव (class, new, int) पर अपने-आप सफ़िक्स जुड़ जाता है। तीन विकल्प हैं: packageName, className और useLombok, जो @Data और हाथ से लिखे getter/setter में स्विच करता है। Lombok चालू होने पर पूरा बॉयलरप्लेट ब्लॉक कुछ एनोटेशनों में सिमट जाता है।
इस कंकाल की दो सीमाएँ पहले जान लेना ठीक है। पहली, यह कोई Jackson/Gson एनोटेशन जनरेट नहीं करता — @JsonProperty नहीं, @JsonIgnoreProperties नहीं, @JsonFormat नहीं, क्योंकि एनोटेशन तय करते हैं deserialization कैसे चलेगी, और सैंपल पेलोड आपकी एक्ज़ीक्यूशन पॉलिसी नहीं बता सकता। दूसरी, रिवर्स कन्वर्ज़न नहीं है: Java क्लास से JSON निकालना दायरे से बाहर है — JSON to TypeScript वाले रास्ते जैसा ही एक-दिशीय डिज़ाइन।
2. पहला सुधार: Double पैसे के लिए टाइप नहीं है
जनरेटर "price": 19.99 को Double में मैप करता है। कंपाइल होता है, असाइन होता है, फिर भी टाइप ग़लत है। बाइनरी फ़्लोटिंग पॉइंट 0.1 को दर्शा नहीं सकता, यानी ज़्यादातर दशमलव भिन्नों को भी नहीं:
System.out.println(19.99); // 19.99 — ठीक दिखता है
double a = 0.1 + 0.2;
System.out.println(a); // 0.30000000000000004
System.out.println(a == 0.3); // false
अकेले 19.99 प्रिंट करने पर साफ़ दिखता है क्योंकि Java सबसे छोटी प्रस्तुति की सुंदरीकरण करता है; गड़बड़ी तुरंत सामने आती है जब मान गणित में घुसता है। इस विषय पर तीनों इंजीनियरिंग लेख एक स्वर में हैं: क़ीमतें, रक़में और दरें BigDecimal होनी चाहिए, स्ट्रिंग से बनी — new BigDecimal("19.99"), कभी भी new BigDecimal(19.99) नहीं, जो double की गड़बड़ी सीधे अंदर ले आता है।
यांत्रिक सुधार है जनरेशन के बाद एक पास: सिर्फ़ पैसे वाले फ़ील्ड में Double की जगह BigDecimal। उसी जड़ की एक और जाल: 2^53 से बड़े पूर्णांक (ऑर्डर नंबर, snowflake ID) Double से जाते हुए चुपचाप अंतिम अंक खोते हैं — 20 अंकों की संख्या के अंतिम तीन अंक खोना मापा गया, दोहराया जा सकने वाला नतीजा है, और तंत्र वही है जो JSON बड़े पूर्णांकों की सटीकता क्यों खोता है के पीछे है। वे फ़ील्ड String या Long में मैप करो, अंक टेक्स्ट बनकर आएँ-जाएँ।
| फ़ील्ड का असली अर्थ | जनरेटर देता है | बदलें |
|---|---|---|
| पैसा, दरें | Double |
BigDecimal (स्ट्रिंग कंस्ट्रक्टर) |
| ऑर्डर नंबर, ID (2^53 पार कर सकते हैं) | Double |
String या Long |
| गिनती, पेज नंबर | Double |
Integer / Long |
| असल में वैज्ञानिक फ़्लोट | Double |
रहने दें |
3. दूसरा सुधार: नाम न टिकें तो @JsonProperty से पिन करो
order_id को orderId बदलना सद्भावना भरा अनुमान है: जनरेटर मानता है कि आप Java तरफ़ camelCase चाहते हैं। पर Jackson डिफ़ॉल्ट में JSON की को फ़ील्ड नाम से अक्षरशः मिलाता है। मेल न खाए तो कुछ भी फेल नहीं होता — orderId बस null रह जाता है। दो उपाय:
फ़ील्ड स्तर पर — @JsonProperty जोड़ो, एक एनोटेशन एक मैपिंग पिन करता है:
public class Root {
@JsonProperty("order_id")
private Long orderId;
}
ग्लोबल — ObjectMapper की नेमिंग स्ट्रैटेजी बदलो, पूरे प्रोजेक्ट में फ़ील्ड-दर-फ़ील्ड एनोटेशन नहीं चाहिए:
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
चुनाव सतह के हिसाब से: एक निश्चित API — फ़ील्ड स्तर; पूरी टीम snake_case सर्विस से बात करती है — ग्लोबल स्ट्रैटेजी। एक नकारात्मक तथ्य यहाँ अपना पैराग्राफ़ पाता है — ग़लत नाम की फेलिंग "चुपचाप null" होती है, एक्सेप्शन नहीं। jsonformater का टूल पेज इसी को जनरेटर के होने का कारण मानता है: यांत्रिक उतारना ही वह जगह है जहाँ यह त्रुटि-दर सबसे ऊपर होती है। इसलिए एनोटेशन जोड़ने के बाद कम से कम एक assertion टेस्ट लिखो कि मुख्य फ़ील्ड null नहीं हैं।
4. तीसरा सुधार: टाइप इरेज़र — नंगी List में कभी deserialise न करें
जनरेटर आपको List<Item> का फ़ील्ड डिक्लरेशन देता है, पर वह लिस्ट असल में भरता Jackson है। टॉप लेवल पर ऐसे deserialise करना सीधा टाइप-इरेज़र की जाल में गिरना है:
// ग़लत: रनटाइम पर एलिमेंट LinkedHashMap होते हैं
List<Item> items = mapper.readValue(json, List.class);
// सही: TypeReference जेनेरिक पैरामीटर बचाता है
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});
readValue(json, List.class) कंपाइल होता है, चलता भी है, पर Java जेनेरिक सिर्फ़ कंपाइल टाइम पर होते हैं। रनटाइम पर Jackson सिर्फ़ List देखता है, तो एलिमेंट LinkedHashMap बनकर बैठ जाते हैं। एरर तुरंत नहीं फटता — कहीं दूर पहले item.getSku() पर फटता है, ClassCastException ग़लती की जगह से बहुत दूर। परिदृश्य के हिसाब से तीन सही रूप:
// 1. टॉप लेवल खुद अरे है
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});
// 2. टॉप-लेवल ऑब्जेक्ट का कोई फ़ील्ड
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});
// 3. स्पष्ट JavaType निर्माण
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);
Gson तरफ़ समकक्ष TypeToken है। तीनों गाइड इसे अलग एंट्री बताती हैं, क्योंकि यह देर से फेल होता है और दूर रिपोर्ट करता है।
5. चौथा सुधार: अज्ञात फ़ील्ड — API के विकास को deserialization न तोड़ने दें
API में नया फ़ील्ड जुड़ना सबसे सामान्य विकास है। पर Jackson का डिफ़ॉल्ट व्यवहार अज्ञात फ़ील्ड पर UnrecognizedPropertyException फेंकना है — साथी एक फ़ील्ड जोड़ता है और आपकी सर्विस एरर देने लगती है। दो स्तर पर इसे बंद करें:
// क्लास स्तर: सिर्फ़ इस क्लास में अज्ञात सहन करो
@JsonIgnoreProperties(ignoreUnknown = true)
public class Root { ... }
// ग्लोबल: पूरा mapper सहन करता है
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
बंद करने के बाद एक दर्पण-समस्या आती है: सहनशीलता ग़लत स्पेलिंग वाले फ़ील्ड नाम भी चुप करा देती है, और पिछले सेक्शन का "चुपचाप null" फेलिंग-मोड खुला चला जाता है। मध्यमार्ग: प्रोडक्शन में सहनशीलता बंद, टेस्ट सूट में चालू रखो, ताकि स्पेलिंग की ग़लतियाँ टेस्टिंग में ही सामने आएँ। Gson डिफ़ॉल्ट में अज्ञात फ़ील्ड नज़रअंदाज़ करता है — इसीलिए तेज़ इंटीग्रेशन में बेफ़िक्र लगता है, और उस आराम की कीमत है कि साथी ने क्या जोड़ा, आप कभी नहीं जानोगे।
6. पाँचवाँ सुधार: तारीखें और नो-आर्ग कंस्ट्रक्टर
जनरेटर created_at को String में मैप करता है, जो दरअसल सबसे सुरक्षित डिफ़ॉल्ट है: JSON में तारीख बस स्ट्रिंग है, और तार पर फ़ॉर्मैट ISO-8601, Unix टाइमस्टैम्प या कस्टम — सैंपल नहीं बता सकता आप कौन-सा चाहते हैं। असली डेट टाइप चाहिए तो दो लाइनें जोड़ें:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// सीरियलाइज़ेशन में टाइमस्टैम्प अरे की जगह ISO-8601 दे
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
public class Root {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
}
नो-आर्ग कंस्ट्रक्टर दूसरी अनिवार्य जाँच है: Jackson deserialization इसी से ऑब्जेक्ट बनाकर, फिर फ़ील्ड एक-एक कर set करके करता है। आप पैरामीटर वाला कंस्ट्रक्टर लिखेंगे तो डिफ़ॉल्ट ग़ायब — deserialization तुरंत फेल। Lombok के साथ @NoArgsConstructor जोड़ें; Java 17 के record पर पूरी समस्या Jackson 2.12+ के record सपोर्ट को सौंपें, जो कंस्ट्रक्टर पैरामीटर से मिलान करता है और यह शर्त पूरी तरह ख़त्म कर देता है।
7. प्रोडक्शन के दो मुद्दे: ObjectMapper पुनः उपयोग और इनपुट सत्यापन
जनरेटर क्लास देता है; रनटाइम पर दो आर्किटेक्चरल बातें बचती हैं। पहली, ObjectMapper बनाना महँगा है और वह thread-safe है — पूरे ऐप में एक इंस्टैंस साझा करो। मेथड के भीतर new ObjectMapper() कॉल सबसे आम परफ़ॉर्मेंस एंटी-पैटर्न है, और Spring Boot जो इंस्टैंस डिफ़ॉल्ट में इंजेक्ट करता है वह ठीक इसी कारण से है। नेमिंग स्ट्रैटेजी, JavaTimeModule और अज्ञात-फ़ील्ड पॉलिसी — सब उसी एक इंस्टैंस पर रहें, कॉल साइट्स में बिखरें नहीं।
दूसरी, सफल deserialization बस बताती है टाइप मिल गए, वैल्यू वैध हैं यह नहीं। ऋणात्मक क़ीमत, शून्य मात्रा, बिगड़ा ईमेल — Jackson को कुछ नहीं। इनपुट सत्यापन Bean Validation (hibernate-validator) का काम है, जनरेट हुए कंकाल पर एनोटेशन जोड़कर:
public class CreateOrderRequest {
@NotNull
@DecimalMin(value = "0.01")
private BigDecimal price;
@Min(1)
private Integer qty;
@Email
private String contact;
}
Spring में @Valid बिज़नेस लॉजिक तक पहुँचने से पहले रिक्वेस्ट रोक देता है। यह परत ऐसी चीज़ है जो जनरेटर कभी नहीं देगा — सैंपल से वह पढ़ सकता है "फ़ील्ड कैसा दिखता है", पर "फ़ील्ड का बिज़नेस बाध्यबंध क्या है" नहीं।
8. जनरेशन कहाँ ख़त्म, हाथ से लिखना कहाँ शुरू
कब जनरेट करें, कब हाथ से लिखें — सीमा साफ़ है:
- जनरेटर का उपयोग करें: बहु-फ़ील्ड रिस्पॉन्स, एक बार का थर्ड-पार्टी इंटीग्रेशन, तेज़ प्रोटोटाइप। यह ठीक एक श्रेणी की त्रुटियाँ मिटाता है — यांत्रिक उतारने की ग़लतियाँ — और सेकंडों में नाम-टाइप संरेखित कंकाल दे देता है।
- हाथ से लिखें या गहरा बदलाव करें: पैसा और लंबे पूर्णांकों में टाइप बदलाव, नेमिंग स्ट्रैटेजी का ग्लोबल निर्णय, कोर डोमेन ऑब्जेक्ट पर सत्यापन एनोटेशन। ये बिज़नेस निर्णय हैं जो सैंपल पेलोड नहीं ले सकता।
- दोनों की साझा पूर्वशर्त: पहले तय करें कि स्रोत JSON वैध है। टूटा पेलोड से भी जनरेटर ख़ुशी-ख़ुशी क्लास बना देता है, जाल deserialization के समय के लिए छोड़ देता है; पेस्ट करने से पहले हमारे वैलिडेशन पेज से चालान करो। दस सेकंड की बात, किसी भी तरफ़।
जनरेटर "शून्य से कंकाल" कवर करता है। इस लेख के पाँच सुधार "कंकाल से प्रोडक्शन" कवर करते हैं। क्रम उलटा नहीं हो सकता, और कोई क़दम छोड़ा नहीं जा सकता।
FAQ
जनरेट हुई क्लास Spring Boot में सीधे उपयोग हो सकती है?
कंपाइल टाइम पर, हाँ। रन से पहले तीन जोड़ सार्थक हैं: पैसे वाले फ़ील्ड BigDecimal में, snake_case API के लिए नेमिंग स्ट्रैटेजी या @JsonProperty, और API के विकास से बचने को @JsonIgnoreProperties(ignoreUnknown = true)। Bean Validation एनोटेशन आपके बिज़नेस नियमों के मुताबिक़।
जनरेटर हर संख्या को long या BigDecimal की बजाय Double में क्यों मैप करता है?
सैंपल "पूर्णांक बनाम दशमलव" तय कर सकता है, बिज़नेस अर्थ नहीं। एक 2 मात्रा भी हो सकती है, ID भी; 20 अंकों का ऑर्डर नंबर और ¥19.99 दोनों JSON में बस संख्या हैं। सिमेंटिक निर्णय उपयोगकर्ता को छोड़ना फ़ॉरवर्ड जनरेटर की युक्तियुक्त सीमा है — कीमत यह कि सेक्शन 2 की बदलाव तालिका आपको खुद दोहरनी होगी।
Lombok का @Data या हाथ से लिखे getter/setter — कौन-सा?
टीम पहले से Lombok चलाती है तो @Data लो, कम से कम बॉयलरप्लेट। कंपाइल-टाइम प्लगइन या दूसरे एनोटेशन प्रोसेसर से टकराव से बचना हो तो हाथ से लिखो। Jackson के लिए कोई फ़र्क़ नहीं; वह getter/setter या फ़ील्ड विज़िबिलिटी देखता है।
नेस्टेड ऑब्जेक्ट static इनर क्लास बनते हैं — कोई असर है?
Jackson के लिए नहीं; static इनर क्लास स्वतंत्र इंस्टैंटिएट हो सकती है। लेकिन लंबी देखभाल में ज़्यादातर टीमें उन्हें टॉप-लेवल क्लास बनाकर पैकेज में निकाल लेती हैं, पुनः उपयोग और अलग टेस्टिंग के लिए। जनरेटर का इनर-क्लास ढाँचा तेज़ देखने के लिए बना है, जीते-जागते कोडबेस के लिए नहीं।
List फ़ील्ड deserialization पर ClassCastException फेंकता है — कैसे पता करें?
पहले देखें deserialization नंगी List.class से तो नहीं हुई: एलिमेंट असल में LinkedHashMap होते हैं और एरर फ़ील्ड पढ़ते समय फटता है। TypeReference पर जाएँ। फिर भी फेल हो तो हमारे एरर मैसेज लुकअप पेज पर एक्सेप्शन पेस्ट करें और अपनी भाषा की एंट्री अपनाएँ।