JSON থেকে Java ক্লাস: জেনারেটর শুধু কাঠামো দেয়, প্রোডাকশনের জন্য আরও পাঁচটি জিনিস লাগে
সংক্ষেপে: একটি JSON-to-Java জেনারেটর আসলে কী আউটপুট দেয় তার একটি বাস্তব দৃষ্টিভঙ্গি — সংখ্যা Double হয়ে যায়, নেস্টেড অবজেক্ট স্ট্যাটিক ইনার ক্লাস হয়, আর একটি Jackson অ্যানোটেশনও তৈরি হয় না। অর্থের নির্ভুলতা, ফিল্ড রিনেমিং, টাইপ ইরেজার, অজানা ফিল্ড এবং তারিখ — এই পাঁচটি জিনিস আপনাকেই যোগ করতে হবে, প্রতিটির জন্য কপি-পেস্টযোগ্য কোডসহ।

সংক্ষেপে: জেনারেটরের আউটপুট সঙ্গে সঙ্গে কম্পাইল হয়, কিন্তু প্রোডাকশনের উপযোগী নয়। আমাদের জেনারেটরের পরিমাপকৃত ডিফল্ট প্রতিটি JSON সংখ্যাকে Double-এ ম্যাপ করে, নেস্টেড অবজেক্টকে স্ট্যাটিক ইনার ক্লাসে পরিণত করে, আর একটি 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 আলাদা স্ট্যাটিক ইনার ক্লাস। ম্যাপিং নিয়মগুলো, একটি একটি করে:
| JSON-এ যে রূপ | জেনারেট হওয়া Java টাইপ | নোট |
|---|---|---|
"name": "王五" |
String |
স্ট্রিং সবসময় String হয় |
"qty": 2 |
Double |
প্রতিটি সংখ্যা Double হয়, সেকশন ২ দেখুন |
"vip": true |
Boolean |
বক্সড টাইপ, null অনুমোদিত |
"buyer": {...} |
Buyer (স্ট্যাটিক ইনার ক্লাস) |
নেস্টেড অবজেক্ট নিজস্ব ক্লাস হয় |
"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 নেই, কারণ অ্যানোটেশন ঠিক করে ডেসিরিয়ালাইজেশন কীভাবে চলবে, আর একটি স্যাম্পল পেলোড আপনার এক্সিকিউশন পলিসি প্রকাশ করতে পারে না। দ্বিতীয়ত, বিপরীত রূপান্তর নেই: 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 দিয়ে গেলে শেষের ডিজিট চুপচাপ হারায় — ২০ ডিজিটের সংখ্যার শেষ তিন ডিজিট হারানো একটি পরিমাপকৃত, পুনরুৎপাদনযোগ্য ফল, আর প্রক্রিয়াটি 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 টুল পেজ এটিকেই জেনারেটরের অস্তিত্বের কারণ মানে: যান্ত্রিক কপি করার সময়ই এই ত্রুটির হার সর্বোচ্চ। তাই অ্যানোটেশন যোগ করার পরে অন্তত একটি অ্যাসারশন টেস্ট লিখুন যে মূল ফিল্ডগুলো null নয়।
4. তৃতীয় সংশোধন: টাইপ ইরেজার — কখনোই খালি List-এ ডেসিরিয়ালাইজ করবেন না
জেনারেটর আপনাকে List<Item> ফিল্ড ডিক্লারেশন দেয়, কিন্তু সেই লিস্ট সত্যিই যে ভরে তা হলো Jackson। টপ লেভেলে এভাবে ডেসিরিয়ালাইজ করলে সরাসরি টাইপ ইরেজারের ফাঁদে পড়বেন:
// ভুল: রানটাইমে এলিমেন্ট হলো 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, ভুল হওয়ার জায়গা থেকে হাজার মাইল দূরে। পরিস্থিতি অনুযায়ী তিনটি সঠিক রূপ:
// ১. টপ লেভেলই অ্যারে
List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});
// ২. টপ-লেভেল অবজেক্টের কোনো ফিল্ড
JsonNode root = mapper.readTree(json);
List<Item> items = mapper.convertValue(root.get("items"), new TypeReference<List<Item>>() {});
// ৩. স্পষ্ট JavaType নির্মাণ
JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Item.class);
List<Item> items = mapper.readValue(json, type);
Gson-এর দিকে সমতুল্য হলো TypeToken। তিনটি গাইডই এটিকে আলাদা আইটেম হিসেবে তালিকাভুক্ত করে, কারণ এটি দেরিতে ব্যর্থ হয় এবং দূরে রিপোর্ট করে।
5. চতুর্থ সংশোধন: অজানা ফিল্ড — API-র বিবর্তন যেন ডেসিরিয়ালাইজেশন ভাঙতে না পারে
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 ডেসিরিয়ালাইজ করে এর মাধ্যমে অবজেক্ট তৈরি করে, তারপর একে একে ফিল্ড সেট করে। আপনি প্যারামিটারসহ কনস্ট্রাক্টর লিখলে ডিফল্টটি হারিয়ে যায় — ডেসিরিয়ালাইজেশন সঙ্গে সঙ্গে ব্যর্থ হয়। Lombok-এর সঙ্গে @NoArgsConstructor যোগ করুন; Java 17-এর record হলে পুরো সমস্যা Jackson 2.12+ রেকর্ড সাপোর্টের হাতে ছেড়ে দিন, যা কনস্ট্রাক্টর প্যারামিটার দিয়ে মেলে এবং এই শর্ত পুরোপুরি তুলে দেয়।
7. প্রোডাকশনের দুটি প্রশ্ন: ObjectMapper পুনর্ব্যবহার ও ইনপুট যাচাই
জেনারেটর ক্লাস দেয়; রানটাইমে আরও দুটি স্থাপত্য-স্তরের বিষয় থাকে। প্রথমত, ObjectMapper তৈরি করা ব্যয়বহুল এবং এটি thread-safe — পুরো অ্যাপ্লিকেশনে একটি ইনস্ট্যান্স ভাগ করুন। মেথডের ভেতরে new ObjectMapper() ডাকা সবচেয়ে প্রচলিত পারফরম্যান্স অ্যান্টি-প্যাটার্ন, আর Spring Boot ডিফল্টে যে ইনস্ট্যান্স ইনজেক্ট করে সেটি ঠিক এই কারণেই আছে। নেমিং স্ট্র্যাটেজি, JavaTimeModule এবং অজানা-ফিল্ড পলিসি — সবই সেই এক ইনস্ট্যান্সে থাকুক, কল সাইটে ছড়িয়ে নয়।
দ্বিতীয়ত, সফল ডেসিরিয়ালাইজেশন শুধু বলে টাইপ মিলেছে, মান বৈধ বলে নয়। ঋণাত্মক দাম, শূন্য পরিমাণ, ত্রুটিপূর্ণ ইমেইল — 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 বৈধ। ভাঙা পেলোড থেকেও জেনারেটর আন্তরিকভাবে ক্লাস বানায়, ফাঁদটা রেখে যায় ডেসিরিয়ালাইজেশনের জন্য; পেস্ট করার আগে আমাদের ভ্যালিডেশন পেজে একবার চালিয়ে নিন। দশ সেকেন্ডের ব্যাপার, যেভাবেই হোক।
জেনারেটর কভার করে "শূন্য থেকে কাঠামো"। এই নিবন্ধের পাঁচটি সংশোধন কভার করে "কাঠামো থেকে প্রোডাকশন"। ক্রম উল্টানো যায় না, আর কোনো ধাপই বাদ দেওয়া যায় না।
FAQ
জেনারেট হওয়া ক্লাস Spring Boot-এ সরাসরি ব্যবহার করা যায়?
কম্পাইল টাইমে যায়। রানটাইমের আগে তিনটি সংযোজন প্রাপ্য: অর্থ-ফিল্ড BigDecimal-এ, snake_case API-র জন্য নেমিং স্ট্র্যাটেজি বা @JsonProperty, আর API-র বিবর্তন টিকিয়ে রাখতে @JsonIgnoreProperties(ignoreUnknown = true)। Bean Validation অ্যানোটেশন আপনার বিজনেস নিয়ম মেনে চলবে।
জেনারেটর কেন প্রতিটি সংখ্যাকে long বা BigDecimal-এর বদলে Double-এ ম্যাপ করে?
স্যাম্পল "পূর্ণসংখ্যা না দশমিক" আলাদা করতে পারে, বিজনেস অর্থ পারে না। একটি 2 পরিমাণও হতে পারে, ID-ও হতে পারে; ২০ ডিজিটের অর্ডার নম্বর আর ¥19.99 JSON-এ দুটোই শুধু সংখ্যা। সেমান্টিক সিদ্ধান্ত ব্যবহারকারীর হাতে ছেড়ে দেওয়া একটি ফরোয়ার্ড জেনারেটরের যুক্তিসঙ্গত সীমা — মূল্য হলো সেকশন ২-এর প্রতিস্থাপন টেবিলটা নিজেই ঘুরে দেখতে হবে।
Lombok-এর @Data না হাতে লেখা getter/setter — কোনটি?
টিম আগে থেকেই Lombok ব্যবহার করলে @Data নিন, সর্বনিম্ন বয়লারপ্লেট। কম্পাইল-টাইম প্লাগইন বা অন্য অ্যানোটেশন প্রসেসরের সঙ্গে সংঘর্ষ এড়াতে চাইলে হাতে লিখুন। Jackson-এর কাছে দুটোই সমান; সে দেখে getter/setter বা ফিল্ডের ভিজিবিলিটি।
নেস্টেড অবজেক্ট স্ট্যাটিক ইনার ক্লাস হয় — কি সমস্যা?
Jackson-এর জন্য না; স্ট্যাটিক ইনার ক্লাস স্বাধীনভাবে ইনস্ট্যান্সিয়েট করা যায়। দীর্ঘমেয়াদি রক্ষণাবেক্ষণে অধিকাংশ টিম সেগুলোকে টপ-লেভেল ক্লাস করে প্যাকেজে তুলে নেয়, পুনর্ব্যবহার ও আলাদা টেস্টিং-এর জন্য। জেনারেটরের ইনার-ক্লাস বিন্যাস দ্রুত দেখার জন্য বানানো, চলমান কোডবেসের জন্য নয়।
List ফিল্ড ডেসিরিয়ালাইজেশনে ClassCastException ছোড়ে — কীভাবে খুঁজে বের করব?
প্রথমে দেখুন ডেসিরিয়ালাইজেশন খালি List.class ব্যবহার করেছে কি: এলিমেন্ট আসলে LinkedHashMap, ফিল্ড পড়ার সময় এরর ছোড়ে। TypeReference-এ যান। তবুও ব্যর্থ হলে আমাদের এরর মেসেজ লুকআপ পেজে এক্সেপশন পেস্ট করুন এবং আপনার ভাষার এন্ট্রি অনুসরণ করুন।