JSON به کلاس جاوا: مولد فقط اسکلت میدهد، برای تولید پنج چیز دیگر لازم است
جمعبندی: نگاهی عملی به آنچه یک مولد JSON-to-Java واقعاً تولید میکند — اعداد Double میشوند، اشیای تو در تو کلاسهای داخلی static میشوند و حتی یک حاشیهنویسی Jackson هم تولید نمیشود. دقت پولی، تغییر نام فیلدها، پاکشدن نوع، فیلدهای ناشناخته و تاریخها پنج چیزی هستند که باید خودتان اضافه کنید، هرکدام با کد آماده برای کپی.

خلاصه: خروجی مولد بلافاصله کامپایل میشود، اما آماده تولید نیست. تنظیمات پیشفرض اندازهگیریشده مولد ما هر عدد JSON را به Double نگاشت میکند، اشیای تو در تو را به کلاسهای داخلی static تبدیل میکند و هیچ حاشیهنویسی Jackson تولید نمیکند. دقت پولی، تغییر نام فیلدها، پاکشدن نوع، فیلدهای ناشناخته و تاریخها — پنج چیزی که هرگز بهجای شما انجام نمیدهد.
تبدیل پاسخ یک API به کلاسهای جاوا کار مکانیکی است: خواندن JSON، رونویسی فیلدها، پیچیدن آنها در getter و setter. هزینه کار مکانیکی همان چاقوی نامرئی است — نام پراپرتی که با یک حرف اشتباه رونویسی شود خطای کامپایل نمیدهد. فیلد بیسروصدا null میرسد، و شما آن را در لاگ تولید کشف میکنید. تولید ساختار کلاس برای این وجود که نامها و نوعها از روز اول همراستا باشند. اما مولد فقط میتواند آنچه را در نمونه دیده میشود همراستا کند.
1. اول مولد را اجرا کنید: واقعاً چه میدهد
یک پاسخ نماینده را در ابزار JSON به 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 میشود، بخش ۲ را ببینید |
"vip": true |
Boolean |
نوع بستهبندیشده، null مجاز |
"buyer": {...} |
Buyer (کلاس داخلی static) |
اشیای تو در تو کلاس خودشان میشوند |
"items": [...] |
List<Item> |
نوع عضو از اولین عضو گرفته میشود |
order_id |
orderId |
snake_case به camelCase تبدیل میشود |
نام کلاسها PascalCase میگیرند (Root)، فیلدها camelCase (orderId)، و برخورد با کلمات کلیدی جاوا (class, new, int) خودکار پسوند میگیرد. سه گزینه وجود دارد: packageName، className و useLombok که بین @Data و getter/setter دستی جابهجا میشود. با فعال بودن Lombok کل بلوک کد تکراری به چند حاشیهنویسی فشرده میشود.
دو مرز این اسکلت را بهتر است از قبل بدانید. اول، هیچ حاشیهنویسی Jackson/Gson تولید نمیکند — نه @JsonProperty، نه @JsonIgnoreProperties، نه @JsonFormat، زیرا حاشیهنویسیها تعیین میکنند deserialization چگونه اجرا شود، و یک payload نمونه نمیتواند سیاست اجرای شما را آشکار کند. دوم، تبدیل معکوس وجود ندارد: استخراج JSON از کلاس جاوا خارج از توان این ابزار است، همان طراحی یکطرفه مسیر JSON به 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 تمیز به نظر میرسد چون جاوا زیباسازی کوتاهترین نمایش را اعمال میکند؛ خطا لحظهای که مقدار وارد محاسبه میشود خودش را نشان میدهد. هر سه نوشته مهندسی در این موضوع اتفاقنظر دارند: قیمتها، مبالغ و نرخها باید BigDecimal باشند، ساختهشده از رشته — new BigDecimal("19.99")، نه new BigDecimal(19.99) که خطای double را مستقیم وارد میکند.
اصلاح مکانیکی یک گذر پس از تولید است: Double را فقط در فیلدهای پولی با BigDecimal جایگزین کنید. دامی مرتبط با همان ریشه: اعداد صحیح بزرگتر از 2^53 (شماره سفارشها، شناسههای snowflake) که از Double بگذرند بیسروصدا رقمهای انتهایی را از دست میدهند — از دست رفتن سه رقم آخر یک عدد ۲۰ رقمی نتیجهای اندازهگیریشده و تکرارپذیر است، و سازوکارش همان است که پشت چرا JSON دقت اعداد صحیح بزرگ را از دست میدهد قرار دارد. آن فیلدها را به String یا Long نگاشت کنید و بگذارید رقمها بهصورت متن سفر کنند.
| معنای واقعی فیلد | مولد میدهد | تغییر به |
|---|---|---|
| پول، نرخها | Double |
BigDecimal (سازنده رشتهای) |
| شماره سفارشها، شناسهها (ممکن است از 2^53 بگذرد) | Double |
String یا Long |
| شمارشها، شماره صفحهها | Double |
Integer / Long |
| اعداد اعشاری واقعاً علمی | Double |
حفظ شود |
3. اصلاح شماره دو: نامهای ناهمخوان، با @JsonProperty ثابت شوند
تبدیل order_id به orderId حدسی نیکاندیشانه است: مولد فرض میکند سمت جاوا 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 برهنه 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) کامپایل میشود و اجرا هم، اما جنریکهای جاوا فقط در زمان کامپایل وجود دارند. در زمان اجرا 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، 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، timestamp یونیکس یا چیزی سفارشی باشد — نمونه نمیتواند بگوید کدام را میخواهید. وقتی به نوع تاریخ واقعی نیاز دارید، دو خط اضافه کنید:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// هنگام serialize بهجای آرایه timestamp، 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 اضافه کنید؛ با record جاوا ۱۷، کل مسئله را به پشتیبانی record در Jackson 2.12+ بسپارید که با پارامترهای سازنده تطبیق میدهد و این الزام را کامل حذف میکند.
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. مرز مولد و دستنویس
چه زمانی تولید و چه زمانی دستنویس، خطی روشن است:
- از مولد استفاده کنید: پاسخهای پر فیلد، یکپارچهسازیهای یکباره شخص ثالث، نمونههای سریع. دقیقاً یک کلاس خطا را حذف میکند — خطاهای رونویسی مکانیکی — و در چند ثانیه اسکلتی با نامها و نوعهای همراستا میدهد.
- دستنویس یا بازسازی عمیق: پول و اعداد صحیح بلند نیاز به تغییر نوع دارند، استراتژی نامگذاری به تصمیم سراسری نیاز دارد، اشیای دامنه اصلی به حاشیهنویسی اعتبارسنجی. اینها تصمیمهای کسبوکاریاند که payload نمونه نمیتواند بگیرد.
- پیششرط مشترک هر دو: ابتدا مطمئن شوید JSON منبع معتبر است. مولد از payload خراب هم با کمال میل کلاس تولید میکند و دام را به زمان deserialization میسپارد؛ قبل از چسباندن، آن را از صفحه اعتبارسنجی ما عبور دهید. ده ثانیه، به هر حال.
مولد «از صفر تا اسکلت» را پوشش میدهد. پنج اصلاح این مقاله «از اسکلت تا تولید» را پوشش میدهد. ترتیب معکوس نمیشود و هیچ گامی حذف نمیشود.
FAQ
کلاسهای تولیدشده را میتوان مستقیماً در Spring Boot استفاده کرد؟
در زمان کامپایل، بله. پیش از اجرا سه افزودنی میارزد: فیلدهای پولی به BigDecimal، استراتژی نامگذاری یا @JsonProperty برای APIهای snake_case، و @JsonIgnoreProperties(ignoreUnknown = true) برای دوام آوردن در برابر تکامل API. حاشیهنویسیهای Bean Validation طبق قواعد کسبوکار شما.
چرا مولد هر عدد را به Double نگاشت میکند، نه long یا BigDecimal؟
نمونه «صحیح یا اعشاری» را تشخیص میدهد اما معنای کسبوکاری را نه. یک 2 میتواند تعداد باشد یا شناسه؛ شماره سفارش ۲۰ رقمی و ¥19.99 در JSON هر دو فقط عددند. واگذاری تصمیمهای معنایی به کاربر مرزی منطقی برای یک مولد رو به جلو است — بهایش این است که جدول جایگزینی بخش ۲ را خودتان باید مرور کنید.
@Data لامبوک یا getter/setter دستی — کدام؟
اگر تیم از قبل لامبوک دارد، @Data را بگیرید برای کمترین کد تکراری. اگر ترجیح میدهید از پلاگین زمان کامپایل یا تداخل با پردازندههای حاشیهنویسی دیگر پرهیز کنید، دستی بنویسید. برای Jackson فرقی نمیکند؛ به getter/setter یا قابلیت رؤیت فیلد نگاه میکند.
اشیای تو در تو کلاس داخلی static میشوند — اهمیتی دارد؟
برای Jackson نه؛ کلاس داخلی static میتواند مستقل نمونهسازی شود. برای نگهداری بلندمدت، بیشتر تیمها آنها را بهصورت کلاسهای سطح بالا در یک پکیج بیرون میکشند، برای بازاستفاده و تست ایزوله. چیدمان کلاس داخلی مولد برای نگاه سریع ساخته شده، نه برای یک کدبیس زنده.
یک فیلد List هنگام deserialization خطای ClassCastException میدهد — چگونه ردیابی کنم؟
اول بررسی کنید deserialization از List.class برهنه استفاده کرده یا نه: اعضا در واقع LinkedHashMap هستند و خطا هنگام خواندن فیلد میجهد. به TypeReference سوئیچ کنید. اگر هنوز شکست میخورد، استثنا را در صفحه جستوجوی پیامهای خطا ما بچسبانید و ورودی زبان خود را دنبال کنید.