সরাসরি পর্বে যান
পর্ব ৩৯ · ৫৯-এর মধ্যে · মডিউল ৬

JSON আর serialization — object পাঠানো ও সংরক্ষণ

JSON and Serialization in Java — Jackson, Gson, Records, transient and the Dangers of Java Serialization

Taught in Bangla, this lesson explains serialization in Java: why objects need a shared format, JSON grammar and how Java types map to it, the escaping trap in hand-built JSON, converting records with Jackson's ObjectMapper and Gson, validating API responses, streaming large JSON, and Serializable, transient and serialVersionUID with the dangers of deserializing untrusted data.

এই পর্বে যা শিখবেন

  • Serialization কেন লাগে, আর JSON-এর ব্যাকরণ ও Java টাইপের সাথে তার মানচিত্র
  • হাতে JSON লেখার বিপদ — escaping কেন দরকার, আর একটি ছোট escaper নিজে চালিয়ে দেখা
  • Jackson-এর ObjectMapper আর Gson দিয়ে record ⇄ JSON, তারিখ, অজানা key আর generic List
  • API-র উত্তর record-এ নেওয়া, parse করার পর যাচাই, আর বিশাল JSON-এর জন্য streaming
  • Java-র নিজস্ব serialization: Serializable, transient, serialVersionUID, এর নিরাপত্তা-ঝুঁকি, ObjectInputFilter আর বিকল্প

১ · object থাকে এক প্রোগ্রামের মেমরিতে — serialization কেন লাগে

পর্ব ১৫ থেকে আপনি যত object বানিয়েছেন, সবই থেকেছে একটি জায়গায়: চলমান প্রোগ্রামের heap-এ (পর্ব ৪৪-এ বিস্তারিত দেখবেন)। একটি Order object-এর ভেতরে আছে কয়েকটি field, আর তার items field আসলে একটি reference — মেমরির একটি ঠিকানা, যেখানে একটি List বসে আছে। প্রোগ্রাম বন্ধ হলে এই সব মেমরি ফেরত যায়, object-গুলো মুছে যায়। আর ঠিকানাগুলো অন্য কম্পিউটারে অর্থহীন।

অথচ বাস্তব software-এর বেশিরভাগ কাজই object-কে প্রোগ্রামের বাইরে নেওয়া। একটি খাবার-ডেলিভারি অ্যাপে গ্রাহকের ফোন অর্ডার বানায়, সেটা যায় সার্ভারের Java প্রোগ্রামে, তারপর রেস্টুরেন্টের ট্যাবলেটে। প্রোগ্রাম আলাদা, ভাষাও আলাদা হতে পারে (Kotlin, Java, JavaScript), আর কেউ কারও মেমরি দেখতে পায় না। তাই object-কে একটি সাধারণ, দুই পক্ষের বোঝার মতো ফরম্যাটে লিখে পাঠাতে হয়, আর ওপাশে সেই লেখা পড়ে আবার object বানাতে হয়।

প্রথম কাজটির নাম serialization — object-কে লেখা বা byte-এর একটি সারিতে (series) রূপান্তর। দ্বিতীয়টি deserialization — সেই লেখা থেকে আবার object বানানো। আর যে দরজা দিয়ে দুটি প্রোগ্রাম এভাবে কথা বলে, তাকে বলে API.

ঢাকা থেকে চট্টগ্রামে একটি খাট পাঠাতে চাইলে পুরো খাট ট্রাকে তোলা যায় না। মিস্ত্রি খাট খুলে টুকরোগুলোতে নম্বর লেখেন, সাথে দেন একটি কাগজ: “১ নম্বর পায়া ৩ নম্বর তক্তার বাঁ দিকে”। ওপাশের মিস্ত্রি সেই কাগজ দেখে খাট আবার জোড়া দেন। শর্ত একটাই — দুজনকে একই কাগজের ভাষা বুঝতে হবে। Serialization হলো খাট খুলে নম্বর লেখা, deserialization হলো জোড়া দেওয়া, আর JSON হলো সেই কাগজের সবার জানা ভাষা।

  1. Java objectOrder(...)সার্ভারের মেমরিতে
  2. serialize{"id":"ORD-7"}সবার বোঝার মতো লেখা
  3. নেটওয়ার্ক / ফাইলHTTP, diskশুধু byte চলে
  4. deserializeOrderঅ্যাপের মেমরিতে নতুন object
ওপাশে যে object তৈরি হয়, সেটা আসল object নয় — তার একটি কপি, লেখা থেকে নতুন করে বানানো।

এই ফরম্যাট দুই ধরনের হতে পারে। লেখাভিত্তিক (text) — মানুষ পড়তে পারে, যেমন JSON, XML, CSV. আর binary — শুধু যন্ত্রের জন্য, ছোট আর দ্রুত, যেমন Protocol Buffers বা Java-র নিজস্ব serialization. এই পর্বের প্রথম ভাগ JSON, দ্বিতীয় ভাগ Java-র নিজস্ব binary serialization — পুরোনো কোডে যা আছে, আর যার বিপদ চেনা দরকার।

২০০০-এর দশকের শুরুতে Douglas Crockford JavaScript-এর object লেখার ধরন থেকে একটি ছোট ডেটা-ফরম্যাট আলাদা করে নাম দেন JSON — JavaScript Object Notation. তিনি নিজে বলতেন, তিনি JSON “আবিষ্কার” করেছেন, “উদ্ভাবন” নয় — কারণ নিয়মগুলো আগে থেকেই JavaScript-এ ছিল। আজ JSON-এর আনুষ্ঠানিক মান দুটি: ECMA-404 আর RFC 8259. এর আগে ডেটা আদান-প্রদানে XML-এর রাজত্ব ছিল; সেই গল্প ভাবনার প্রশ্ন ১ আর ২-এ।

২ · JSON-এর ব্যাকরণ — ছয় ধরনের মান

JSON-এর পুরো ব্যাকরণ এক পাতায় ধরে যায়, আর এটাই তার শক্তি। একটি আবহাওয়া API ঢাকার আবহাওয়া জানতে চাইলে এমন একটি উত্তর দিতে পারে (কাল্পনিক উদাহরণ):

{
  "city": "Dhaka",
  "updated": "2026-10-18T09:00:00+06:00",
  "temperature_c": 31.5,
  "humidity": 78,
  "rain_expected": true,
  "alert": null,
  "forecast": [
    { "day": "2026-10-19", "max_c": 32, "min_c": 25, "summary": "হালকা বৃষ্টি" },
    { "day": "2026-10-20", "max_c": 30, "min_c": 24, "summary": "মেঘলা" }
  ]
}

এর ভেতরে মাত্র ছয় ধরনের মান আছে:

JSON মানউদাহরণনিয়ম
object{ "city": "Dhaka" }key–মানের জোড়া; key সবসময় ডাবল কোটের ভেতরে string
array[ 32, 30 ]ক্রমযুক্ত তালিকা; ভেতরে যেকোনো মান, মেশানোও চলে
string"হালকা বৃষ্টি"শুধু ডাবল কোট; ভেতরে ", \ আর নিয়ন্ত্রণ-অক্ষর escape করতে হয়
number31.5, 78, -2e3int আর দশমিকের আলাদা টাইপ নেই; NaN, Infinity, শুরুতে বাড়তি 0 চলে না
true / falsetrueছোট হাতের অক্ষরে, কোট ছাড়া
nullnull“মান নেই”

যা JSON-এ নেই, সেটাও মনে রাখুন, কারণ নতুনরা এখানেই ভুল করে: comment (//) নেই; শেষ উপাদানের পরে বাড়তি কমা (trailing comma) চলে না; একক কোট 'Dhaka' চলে না; key কোট ছাড়া লেখা যায় না; আর তারিখের আলাদা কোনো টাইপ নেই — তারিখ যায় string হিসেবে। RFC 8259 আরও বলে, সিস্টেমের বাইরে আদান-প্রদানের JSON লেখা হবে UTF-8-এ, তাই “হালকা বৃষ্টি”-র মতো বাংলা লেখা সরাসরি রাখা যায়।

মূল ধারণা

JSON শুধু ডেটা বহন করে, আচরণ বা টাইপের নাম নয়। {"city":"Dhaka"} দেখে বোঝার উপায় নেই এটা কোন Java class থেকে এসেছে। কোন JSON কোন টাইপে যাবে, সেই সিদ্ধান্ত নেয় পড়ার দিকের প্রোগ্রাম। এই কারণেই JSON নিরাপদ: লেখা পড়ে প্রোগ্রাম শুধু আপনার ঠিক করা record বানায়, অচেনা কোনো class নয়। অংশ ৮-এ দেখবেন, Java-র নিজস্ব serialization ঠিক এখানেই উল্টো।

৩ · Java object থেকে JSON — কোন জিনিস কোথায় যায়

এবার উল্টো দিক থেকে দেখা যাক: Java-র টাইপগুলো JSON-এ কীভাবে বসে। একটি অনলাইন দোকানের মোবাইল অ্যাপকে সার্ভার একটি অর্ডার পাঠাবে। Java-তে অর্ডারটি record দিয়ে লেখা (পর্ব ২৫):

record Item(String name, int qty, long paisa) {}
record Order(String id, String customer, List<Item> items,
             LocalDate deliveryDate, String note) {}

মেমরিতে এটা একটি object graph — একটি Order, যার field একটি List-কে দেখায়, আর List দেখায় কয়েকটি Item-কে। Serialization-এর কাজ এই গাছটিকে হেঁটে হেঁটে একটি লেখায় সাজানো। ধাপে ধাপে দেখুন:

ধাপ ১মেমরিতে object graph
Order ──┬─ id           = "ORD-7"
        ├─ customer     = "রনি"
        ├─ items ───────▶ List ──┬─▶ Item("চা", 2, 2000)
        │                        └─▶ Item("সিঙ্গারা", 3, 1000)
        ├─ deliveryDate = LocalDate 2026-10-20
        └─ note         = null
reference মানে মেমরির ঠিকানা — বাইরে পাঠানো যায় না
ধাপ ২record → JSON object
{
  "id": ...,
  "customer": ...,
  "items": ...,
  "deliveryDate": ...,
  "note": ...
}
প্রতিটি record component একটি key — নাম হুবহু component-এর নামে
ধাপ ৩String আর সংখ্যা
{
  "id": "ORD-7",
  "customer": "রনি",
  ...
String → ডাবল কোটের ভেতরে (escape করে)
int, long → কোট ছাড়া number: 2, 2000
ধাপ ৪List → array, ভেতরের record → nested object
  "items": [
    { "name": "চা", "qty": 2, "paisa": 2000 },
    { "name": "সিঙ্গারা", "qty": 3, "paisa": 1000 }
  ],
reference-এর জায়গায় পুরো object-টাই লেখার ভেতরে বসে গেল
ধাপ ৫তারিখ আর null — সম্পূর্ণ JSON
{
  "id": "ORD-7",
  "customer": "রনি",
  "items": [ {...}, {...} ],
  "deliveryDate": "2026-10-20",
  "note": null
}
LocalDate → ISO-8601 string; null → null (অথবা key বাদ)
  1. শুরু মেমরিতে। Order-এর items field-এ List নেই, আছে List-এর ঠিকানা। এই ঠিকানা অন্য কম্পিউটারে অর্থহীন, তাই serialization-কে প্রতিটি reference অনুসরণ করে আসল মান পর্যন্ত যেতে হবে।
  2. record হলো object. record স্বচ্ছ — তার সব state হেডারেই ঘোষিত (পর্ব ২৫)। তাই প্রতিটি component সরাসরি একটি key হয়। নাম আলাদা রাখতে চাইলে annotation লাগে (অংশ ৫)।
  3. সরল মান। String যায় ডাবল কোটে; int, long, double যায় কোট ছাড়া। টাকা এখানে long paisa — পয়সায় গোনা পূর্ণসংখ্যা, যাতে দশমিকের গোলমাল না থাকে।
  4. List হলো array. List-এর প্রতিটি উপাদান array-র একটি ঘর, আর প্রতিটি Item record নিজেই একটি ছোট object. Map হলে সেটা হতো JSON object, যার key হতো Map-এর key (string বানিয়ে)।
  5. তারিখ আর null. JSON-এ তারিখের টাইপ নেই, তাই LocalDate যায় "2026-10-20" — পর্ব ৩৮-এ দেখা ISO-8601 ফরম্যাটে। null যায় JSON null হিসেবে; কিছু লাইব্রেরি ডিফল্টে null-এর key পুরোপুরি বাদ দেয় (অংশ ৫-এ Gson).
JavaJSONসাবধানতা
record, সাধারণ classobjectclass-এ লাইব্রেরি সাধারণত getter বা field দেখে (JavaBeans রীতি)
List, Set, arrayarraySet-এর ক্রম নির্দিষ্ট নয়
Map<String, V>objectJSON object-এর key-এর ক্রমের কোনো নিশ্চয়তা নেই
String, enumstringenum যায় নাম হিসেবে: "CASH_OUT"
int, long, doublenumberখুব বড় long অন্য ভাষায় নির্ভুল না-ও থাকতে পারে
booleantrue / false—
nullnull, অথবা key বাদপড়ার দিকে “key নেই” আর “null” আলাদা হতে পারে
LocalDate, Instantstring (ISO-8601)"2026-10-20", "2026-10-18T04:30:00Z" — পর্ব ৩৮
BigDecimal (টাকা)number বা stringstring "150.50" বা পয়সায় পূর্ণসংখ্যা বেশি নিরাপদ

টাকা আর বড় সংখ্যা নিয়ে সাবধান। JSON-এর নিয়মে সংখ্যার সীমা লেখা নেই, কিন্তু পড়ার দিকের প্রোগ্রাম সংখ্যাটা কোন টাইপে রাখবে, সেটা তার ব্যাপার। JavaScript (আর অনেক লাইব্রেরি) সব সংখ্যাকে floating-point double হিসেবে রাখে। ফলে 150.10-এর মতো মান ভেতরে নির্ভুল থাকে না, আর ২৫৩-এর চেয়ে বড় পূর্ণসংখ্যা (যেমন কিছু বড় ID) নীরবে বদলে যায়। Java-তে টাকা রাখেন BigDecimal-এ; JSON-এ পাঠানোর সময় দুটি নিরাপদ পথ — string হিসেবে "150.50", অথবা পয়সায় গোনা পূর্ণসংখ্যা 15050. বিস্তারিত ভাবনার প্রশ্ন ৫-এ।

C/C++/Python জানলে

C-তে struct-কে fwrite দিয়ে সরাসরি ফাইলে লেখা যায়, কিন্তু সেই byte অন্য কম্পিউটারে padding, byte-ক্রম আর pointer-এর কারণে প্রায়ই অর্থহীন — তাই C আর C++-এ JSON-এর জন্য বাইরের লাইব্রেরি লাগে। Python-এ json module ভাষার সাথেই আসে, আর json.dumps/json.loads-এর কাজ Java-তে Jackson-এর writeValueAsString/readValue করে। Python-এর pickle হলো Java serialization-এর আত্মীয় — একই সুবিধা, একই বিপদ: অবিশ্বস্ত pickle খোলা মানে কোড চালানোর ঝুঁকি। Python-এর দিকটা দেখুন CSV আর JSON পর্বে।

৪ · হাতে JSON লেখা — text block, formatted আর escaping-এর ফাঁদ

JSON তো শুধু লেখা — তাহলে লাইব্রেরি ছাড়া নিজেই String জোড়া দিয়ে বানানো যায় না? যায়, আর প্রথমবার দেখতে খুব সহজও লাগে। পর্ব ০৯-এর text block আর formatted দিয়ে:

String json = """
    {"id": "%s", "customer": "%s", "total": %d}
    """.formatted(id, customer, totalPaisa);

যতক্ষণ customer-এর নাম “রনি”, ততক্ষণ সব ঠিক। কিন্তু কেউ নামের ঘরে লিখলেন রনি "R" — একটি কোট চিহ্নসহ। তখন লেখাটা হয় "customer": "রনি "R"". JSON parser প্রথম কোট দেখে string শুরু করে, দ্বিতীয় কোটে শেষ করে, তারপর R দেখে থেমে যায় — এটা কোনো বৈধ মান নয়। আর কেউ ঠিকানা লিখতে গিয়ে Enter চাপলে নামের ভেতরে একটি নতুন লাইন চলে আসে; JSON string-এর ভেতরে কাঁচা নতুন লাইন নিষিদ্ধ। ফলে অ্যাপ অর্ডারটি পড়তেই পারে না।

সমাধান হলো escaping. RFC 8259 অনুযায়ী string-এর ভেতরে তিন রকম অক্ষর সরাসরি রাখা যায় না: ডাবল কোট (\" লিখতে হয়), backslash (\\), আর U+0000 থেকে U+001F পর্যন্ত নিয়ন্ত্রণ-অক্ষর — নতুন লাইন \n, tab \t, বা সাধারণভাবে \u000a-এর মতো। এগুলো পর্ব ০৪-এর escape sequence-এর মতোই দেখতে, কিন্তু এবার নিয়ম JSON-এর। বাংলা বা অন্য Unicode অক্ষর escape না করেই রাখা যায়। নিচের stepper-এ একটি ছোট escaper-এর ভেতর দিয়ে রনি "R" আর তারপর একটি নতুন লাইন ও “মিয়া” — এই নামটি যাচ্ছে:

1static String q(String s) {2  var sb = new StringBuilder("\"");3  for (char c : s.toCharArray()) {4    switch (c) {5      case '"'  -> sb.append("\\\"");6      case '\\' -> sb.append("\\\\");7      case '\n' -> sb.append("\\n");8      default   -> sb.append(c ...);9    }10  }11  return sb.append('"').toString();
অবস্থা
c
sb
  1. শুরুর কোট। JSON string শুরু হয় একটি ডাবল কোট দিয়ে, তাই StringBuilder প্রথমেই " দিয়ে শুরু।
  2. সাধারণ অক্ষর হুবহু। র, ন, ি আর ফাঁকা জায়গা — এগুলোর কোনোটিই কোট, backslash বা নিয়ন্ত্রণ-অক্ষর নয়, তাই default শাখায় যেমন আছে তেমনই যোগ হয়। বাংলা অক্ষর escape করার দরকার নেই।
  3. কোট এলো। " সরাসরি বসালে JSON string এখানেই শেষ হয়ে যেত। তাই বসে দুটি অক্ষর: backslash আর কোট — \".
  4. R — সাধারণ অক্ষর। হুবহু যোগ হলো।
  5. আবার কোট। আবার \". খেয়াল করুন, Java কোডে এটা লিখতে "\\\"" লাগে — Java-র নিজের escape আর JSON-এর escape একসাথে, তাই হাতে লিখলে ভুল হওয়া খুব সহজ।
  6. নতুন লাইন। এক অক্ষরের নতুন লাইন (code 10) JSON string-এ কাঁচা অবস্থায় নিষিদ্ধ। বসে দুটি দৃশ্যমান অক্ষর: backslash আর n. অন্য নিয়ন্ত্রণ-অক্ষর, যাদের জন্য আলাদা case নেই, default শাখায় \u00XX রূপে যায়।
  7. বাকি বাংলা। ম, ি, য়, া — সব হুবহু।
  8. শেষের কোট। শেষে একটি " যোগ করে string বন্ধ। ফল একটি বৈধ JSON string, যা যেকোনো parser পড়ে আবার আসল নামটি — কোট আর নতুন লাইনসহ — ফেরত দেবে।

এবার নিজে চালিয়ে দেখুন — প্রথমে ভুল পথ (সরাসরি জোড়া দেওয়া), তারপর escaper দিয়ে একটি পুরো অর্ডার:

import java.util.*;

record Item(String name, int qty, long paisa) {}

class Main {
    static String q(String s) {                       // JSON string escaper
        var sb = new StringBuilder("\"");
        for (char c : s.toCharArray()) {
            switch (c) {
                case '"'  -> sb.append("\\\"");
                case '\\' -> sb.append("\\\\");
                case '\n' -> sb.append("\\n");
                default   -> sb.append(c < 0x20 ? "\\u%04x".formatted((int) c) : String.valueOf(c));
            }
        }
        return sb.append('"').toString();
    }
    public static void main(String[] args) {
        String customer = "রনি \"R\"\nমিয়া";
        System.out.println("{\"customer\":\"" + customer + "\"}");      // ভুল পথ
        var items = List.of(new Item("চা", 2, 2000), new Item("সিঙ্গারা", 3, 1000));
        var parts = items.stream().map(i -> "{\"name\":%s,\"qty\":%d,\"paisa\":%d}".formatted(q(i.name()), i.qty(), i.paisa())).toList();
        System.out.println("{\"customer\":%s,\"items\":[%s]}".formatted(q(customer), String.join(",", parts)));
    }
}

আউটপুট তিন লাইন। প্রথম দুই লাইন ভুল পথের: {"customer":"রনি "R" আর তার নিচে মিয়া"} — নামের ভেতরের কোট string-কে আগেই বন্ধ করে দিয়েছে, আর কাঁচা নতুন লাইন JSON-টিকে দুই লাইনে ভেঙেছে। কোনো parser এটা পড়বে না। তৃতীয় লাইন escaper দিয়ে বানানো বৈধ JSON: {"customer":"রনি \"R\"\nমিয়া","items":[{"name":"চা","qty":2,"paisa":2000},{"name":"সিঙ্গারা","qty":3,"paisa":1000}]}. এখানে নামের কোট হয়েছে \", নতুন লাইন হয়েছে দুই অক্ষরের \n, আর বাংলা রয়ে গেছে হুবহু। টাকা long paisa-তে, তাই ২০ টাকা লেখা আছে 2000 হিসেবে — দশমিকের কোনো ঝুঁকি নেই। record আর Main public ছাড়া লেখা, কারণ অনলাইন runner ফাইলটিকে অন্য নামে compile করে।

এই escaper শেখার জন্য, ব্যবহারের জন্য নয়। বাস্তবে null, nested Map, তারিখ আর সবচেয়ে কঠিন কাজ — পড়া — সামলাতে হয়। indexOf দিয়ে হাতে মান খোঁজা আরও ভঙ্গুর: key-এর ক্রম বা ফাঁকা জায়গা বদলালেই ভুল মান ওঠে। হাতে লেখা JSON কোড অনেক সময় injection-এর দরজাও খোলে — কেউ নামের ঘরে ","role":"admin লিখে নতুন key ঢুকিয়ে দিতে পারে। নিয়ম সহজ: JSON লেখা আর পড়া — দুটোই লাইব্রেরিকে দিন।

৫ · Jackson আর Gson — লাইব্রেরি দিয়ে JSON

JDK-তে কোনো JSON লাইব্রেরি নেই, তাই Java জগতে দুটি বাইরের library সবচেয়ে প্রচলিত: Jackson (Spring Boot-এর ডিফল্ট) আর Google-এর Gson (Android-এ জনপ্রিয়)। এগুলো প্রজেক্টে যোগ হয় Maven বা Gradle দিয়ে; পর্ব ৪৬-এ বিস্তারিত দেখবেন। আপাতত শুধু চিনে রাখুন, pom.xml-এ এমন কয়েকটি লাইন লাগে:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.2</version>
</dependency>
<dependency>   <!-- java.time (LocalDate, Instant) সমর্থনের জন্য -->
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.17.2</version>
</dependency>

সংস্করণ নম্বরটি উদাহরণ মাত্র; Maven Central-এ সর্বশেষ 2.x দেখে নিন। এই পর্বের Jackson কোড 2.x-এর (com.fasterxml.jackson package) জন্য লেখা — Jackson-এর নতুন বড় সংস্করণ 3.x-এ package-এর নাম আর কিছু ডিফল্ট বদলেছে। এই লাইব্রেরিগুলো অনলাইন runner-এ নেই, তাই নিচের কোডগুলো চালানো যাবে না; পাশে “প্রত্যাশিত আউটপুট” লেখা আছে।

ObjectMapper — record থেকে JSON, JSON থেকে record

Jackson-এর কেন্দ্র একটি class: ObjectMapper. একবার বানিয়ে পুরো প্রোগ্রামে ব্যবহার করা হয়। Jackson 2.12 থেকে record সরাসরি সমর্থিত — component-এর নাম দেখে key মেলায়, আর পড়ার সময় canonical constructor ডেকে object বানায়।

var mapper = new ObjectMapper();
var order = new Order("ORD-7", "রনি", List.of(new Item("চা", 2, 2000)));

String json = mapper.writeValueAsString(order);          // serialize
System.out.println(json);
// প্রত্যাশিত আউটপুট:
// {"id":"ORD-7","customer":"রনি","items":[{"name":"চা","qty":2,"paisa":2000}]}

Order back = mapper.readValue(json, Order.class);        // deserialize
System.out.println(back.equals(order));                  // প্রত্যাশিত: true

এখানে Order তিন component-এর (id, customer, items). দুটি জিনিস খেয়াল করুন। এক, কোট বা নতুন লাইনের escaping নিয়ে আপনাকে ভাবতে হয়নি। দুই, back একটি নতুন object, কিন্তু record-এর equals মান তুলনা করে, তাই true. key-এর ক্রম সাধারণত component-এর ক্রমে থাকে, তবে JSON object-এর key-এর ক্রমের কোনো অর্থ নেই — ক্রমের ওপর নির্ভর করে কোড লিখবেন না।

Generic List পড়া — TypeReference. JSON-এর শুরুতেই যদি array থাকে ([{...},{...}]), তাহলে readValue(json, List.class) লিখলে Jackson জানে না ভেতরে কী টাইপ, কারণ পর্ব ২৮-এর type erasure-এ List<Item>-এর Item চলার সময় মুছে যায়। ফল হয় Item-এর বদলে Map-এর List. সমাধান একটি type token:

List<Item> items = mapper.readValue(json, new TypeReference<List<Item>>() {});

শেষের {} লক্ষ করুন — এটা একটি ছোট anonymous class (পর্ব ২৭)। subclass-এর superclass-এর generic তথ্য class file-এ থেকে যায়, তাই Jackson reflection দিয়ে সেখান থেকে List<Item> পড়ে নেয়। Gson-এ একই কৌশলের নাম TypeToken.

যে annotation আর সেটিং প্রায় প্রতিদিন লাগে

@JsonIgnoreProperties(ignoreUnknown = true)                 // অজানা key এলে উপেক্ষা
record Txn(@JsonProperty("trx_id") String trxId,             // JSON-এ trx_id, Java-তে trxId
           TxnType type,
           BigDecimal amount,
           Instant time,
           @JsonIgnore String internalNote) {}               // JSON-এ যাবেও না, আসবেও না

var mapper = new ObjectMapper()
        .registerModule(new JavaTimeModule())                    // java.time সমর্থন
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // তারিখ ISO string হিসেবে
  • @JsonProperty("trx_id") — JSON-এর key আর Java-র নাম আলাদা হলে।
  • @JsonIgnore — ভেতরের কোনো তথ্য (পাসওয়ার্ডের hash, ভেতরের নোট) যেন কখনো JSON-এ বাইরে না যায়। এগুলো পর্ব ৪৫-এর annotation, যা Jackson চলার সময় পড়ে।
  • অজানা key — Jackson 2.x-এ FAIL_ON_UNKNOWN_PROPERTIES ডিফল্টে চালু: JSON-এ record-এর বাইরের একটি key (ধরুন সার্ভার নতুন "fee" যোগ করল) এলেই UnrecognizedPropertyException. বাইরের API পড়ার সময় সাধারণত @JsonIgnoreProperties(ignoreUnknown = true) দেওয়া হয়, যাতে সার্ভার নতুন field যোগ করলে আপনার অ্যাপ ভেঙে না যায়।
  • তারিখ — JavaTimeModule না দিলে Jackson 2.x LocalDate বা Instant লিখতে গিয়ে exception দেয়। module দিলেও ডিফল্টে তারিখ যায় সংখ্যা বা array হিসেবে; WRITE_DATES_AS_TIMESTAMPS বন্ধ করলে যায় "2026-10-20"-এর মতো ISO string.

Gson — Google-এর সরল বিকল্প

var gson = new Gson();
String json = gson.toJson(order);
// প্রত্যাশিত: {"id":"ORD-7","customer":"রনি","items":[{"name":"চা","qty":2,"paisa":2000}]}
Order back = gson.fromJson(json, Order.class);
List<Item> list = gson.fromJson(arrJson, new TypeToken<List<Item>>() {}.getType());
দিকJacksonGson
মূল methodwriteValueAsString, readValuetoJson, fromJson
record সমর্থন2.12 থেকে2.10 থেকে
null fieldডিফল্টে "note":null লেখেডিফল্টে key-টাই বাদ দেয়
অজানা keyডিফল্টে exception (2.x)ডিফল্টে উপেক্ষা করে
java.timeJavaTimeModule যোগ করতে হয়নিজে TypeAdapter লিখতে হয়
কোথায় বেশিSpring Boot, সার্ভারAndroid, ছোট প্রজেক্ট

দুটি লাইব্রেরিই ভালো — একটি প্রজেক্টে একটিই বেছে নিন, যাতে নিয়ম মিশে না যায়।

Jackson একটি JSON লেখা থেকে record কীভাবে বানায়, ধাপে ধাপে দেখুন:

  1. লেখাbyte (UTF-8)নেটওয়ার্ক থেকে
  2. tokenJsonParserটুকরো করা
  3. নাম মেলানোtrx_id → trxIdcomponent খোঁজা
  4. টাইপ বদলBigDecimal, Instantলেখা → Java মান
  5. recordnew Txn(...)canonical constructor
ইনপুটAPI থেকে আসা লেখা
{"trx_id":"BK7X9Q2","type":"CASH_OUT","amount":"1000.00",
 "time":"2026-10-18T04:15:30Z","fee":"18.50"}
token-এর সারিJsonToken
START_OBJECT
FIELD_NAME "trx_id"   VALUE_STRING "BK7X9Q2"
FIELD_NAME "type"     VALUE_STRING "CASH_OUT"
FIELD_NAME "amount"   VALUE_STRING "1000.00"
FIELD_NAME "time"     VALUE_STRING "2026-10-18T04:15:30Z"
FIELD_NAME "fee"      VALUE_STRING "18.50"
END_OBJECT
নাম মেলানোrecord-এর component
"trx_id" → trxId    (@JsonProperty)
"type"   → type
"amount" → amount
"time"   → time
"fee"    → ??? অজানা
ignoreUnknown = true থাকলে উপেক্ষা, না থাকলে UnrecognizedPropertyException
টাইপ বদলcomponent-এর টাইপ দেখে
"CASH_OUT"             → TxnType.CASH_OUT
"1000.00"              → new BigDecimal("1000.00")
"2026-10-18T04:15:30Z" → Instant   (JavaTimeModule লাগে)
internalNote           → null      (@JsonIgnore, JSON-এ নেই)
object তৈরিএকমাত্র দরজা
new Txn("BK7X9Q2", CASH_OUT, 1000.00, 2026-10-18T04:15:30Z, null)
   └─▶ compact constructor-এর যাচাই চলে
Txn[trxId=BK7X9Q2, type=CASH_OUT, amount=1000.00, ...]
  1. লেখা আসে। নেটওয়ার্ক থেকে আসে byte; UTF-8 ধরে সেগুলো অক্ষরে রূপ নেয়।
  2. Token-এ ভাঙা। Jackson-এর নিচের স্তর JsonParser লেখাকে ছোট ছোট token-এ ভাঙে: object শুরু, key-এর নাম, string মান, object শেষ। এখানেই ব্যাকরণ যাচাই হয় — বাড়তি কমা বা ভাঙা কোট থাকলে এখানেই exception.
  3. নাম মেলানো। প্রতিটি key-এর জন্য record-এর একটি component খোঁজা হয়। trx_id মেলে @JsonProperty-এর কারণে। fee-এর কোনো component নেই — এখানে ignoreUnknown ঠিক করে দেয় অ্যাপ ভাঙবে, না এগোবে।
  4. টাইপ বদল। component-এর ঘোষিত টাইপ দেখে লেখা থেকে Java মান: enum-এর নাম থেকে enum, string থেকে BigDecimal (নির্ভুল), ISO string থেকে Instant. JSON-এ না থাকা component পায় null (primitive হলে 0) — Jackson নিজে থেকে “বাধ্যতামূলক” যাচাই করে না।
  5. Canonical constructor. সব মান হাতে পেলে Jackson record-এর canonical constructor ডাকে। তাই compact constructor-এ লেখা যাচাই (অংশ ৬) JSON থেকে আসা প্রতিটি object-এর জন্যও চলে। যাচাই ব্যর্থ হলে Jackson exception-টিকে নিজের একটি JsonMappingException-এ মুড়ে ছোড়ে — কোনো অবৈধ record তৈরি হয় না।

৬ · API-র উত্তর record-এ — লেনদেনের ইতিহাস, যাচাই আর বিশাল JSON

একটি bKash-ধাঁচের মোবাইল ওয়ালেট অ্যাপ সার্ভারের কাছে গত কয়েকটি লেনদেন চাইল। সার্ভারের উত্তর (কাল্পনিক, কিন্তু বাস্তবের মতো):

{
  "status": "OK",
  "wallet": "01700000000",
  "transactions": [
    { "trx_id": "BK7X9Q2", "type": "SEND_MONEY", "amount": "500.00",
      "time": "2026-10-18T04:15:30Z" },
    { "trx_id": "BK7X9Q3", "type": "CASH_OUT", "amount": "1000.00",
      "fee": "18.50", "time": "2026-10-18T06:02:11Z" }
  ]
}

টাকা এসেছে string হিসেবে (অংশ ৩-এর পরামর্শ মেনে), সময় ISO-8601 আকারে UTC-তে। Java-তে এর জন্য record লিখি, আর যাচাই রাখি compact constructor-এ (পর্ব ২৫):

enum TxnType { SEND_MONEY, CASH_OUT, PAYMENT }

@JsonIgnoreProperties(ignoreUnknown = true)
record Txn(@JsonProperty("trx_id") String trxId, TxnType type,
           BigDecimal amount, Instant time) {
    Txn {
        Objects.requireNonNull(trxId, "trx_id missing");
        Objects.requireNonNull(time, "time missing");
        if (amount == null || amount.signum() <= 0)
            throw new IllegalArgumentException("bad amount: " + amount);
    }
}

@JsonIgnoreProperties(ignoreUnknown = true)
record TxnHistory(String status, List<Txn> transactions) {
    TxnHistory { transactions = List.copyOf(transactions); }     // defensive copy
}

TxnHistory h = mapper.readValue(body, TxnHistory.class);
h.transactions().forEach(t -> System.out.println(t.trxId() + " " + t.amount()));
// প্রত্যাশিত আউটপুট:
// BK7X9Q2 500.00
// BK7X9Q3 1000.00

দ্বিতীয় লেনদেনে fee আছে, কিন্তু Txn-এ এমন কোনো component নেই — ignoreUnknown থাকায় সেটা নীরবে উপেক্ষিত হলো। wallet-ও একই কারণে উপেক্ষিত। ফলে সার্ভার নতুন তথ্য যোগ করলেও পুরোনো অ্যাপ চলতে থাকে।

Parse করার পর যাচাই — কেন আলাদা ধাপ

Parse সফল হওয়া মানে শুধু “লেখাটা ব্যাকরণমতো JSON আর টাইপ মিলেছে”। এর মানে এই নয় যে ডেটা সঠিক। অংশ ৫-এর stepper-এ দেখেছেন, JSON-এ একটি key না থাকলে Jackson সেই component-এ null বসায় — ভুল বা ক্ষতিকর ডেটাও নিখুঁতভাবে parse হয়। তাই যাচাইয়ের দুটি স্তর রাখুন:

  • গঠনের যাচাই (compact constructor): বাধ্যতামূলক field আছে কি না, টাকা ধনাত্মক কি না, ফোন নম্বর ১১ অঙ্কের কি না। record হলে এটা যেকোনো পথে আসা object-এর জন্য চলে।
  • ব্যবসার নিয়মের যাচাই (service-এর ভেতরে): এই ওয়ালেটের দৈনিক সীমা পার হলো কি না, এই trx_id আগে এসেছে কি না। এগুলোর জন্য ডেটাবেস বা অন্য তথ্য লাগে, তাই constructor-এ নয়।

বিশেষ করে বাইরে থেকে আসা JSON — মোবাইল অ্যাপের request, অন্য কোম্পানির API — সবসময় অবিশ্বস্ত ধরুন।

অ্যাপের সেটিং সংরক্ষণ

JSON শুধু নেটওয়ার্কের জন্য নয়। একটি ডেস্কটপ অ্যাপ ব্যবহারকারীর পছন্দ (থিম, লেখার আকার, ভাষা) একটি ছোট ফাইলে রাখতে পারে, যা মানুষও খুলে পড়তে ও বদলাতে পারে:

record AppSettings(String theme, int fontSize, String language) {}

Path file = Path.of("settings.json");
String text = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(settings);
Files.writeString(file, text);                                   // পর্ব ৩৭
AppSettings loaded = mapper.readValue(Files.readString(file), AppSettings.class);

writerWithDefaultPrettyPrinter() JSON-কে কয়েক লাইনে সাজিয়ে লেখে, যাতে মানুষ সহজে পড়তে পারে।

বিশাল JSON — streaming

readValue পুরো JSON একবারে পড়ে সব object মেমরিতে বানায়। কিন্তু একটি ব্যাংক রাতে লাখো লেনদেনের ২ GB-র ফাইল পাঠালে সব একসাথে মেমরিতে তুলতে গিয়ে OutOfMemoryError হতে পারে। তখন লাগে streaming parser: ফাইল একটু একটু করে পড়া, একটি token বা একটি লেনদেন হাতে নিয়ে কাজ করা, তারপর সেটা ছেড়ে দেওয়া। Jackson-এ এটা সেই নিচের স্তর JsonParser (nextToken() ডেকে ডেকে এগোনো), অথবা mapper.readerFor(Txn.class).readValues(...) দিয়ে একটি একটি record পাওয়া; Gson-এ JsonReader.

৭ · Java-র নিজস্ব serialization — Serializable, transient আর serialVersionUID

JSON আসার অনেক আগে, ১৯৯৭ সালের JDK 1.1-এ Java নিজের একটি serialization ব্যবস্থা এনেছিল। উদ্দেশ্য ছিল যেকোনো object graph-কে কোনো বাড়তি কোড ছাড়াই byte-এ রূপ দেওয়া — দূরের কম্পিউটারের method ডাকা (RMI) বা object ফাইলে রাখার জন্য। ব্যবহার খুব সহজ: class-এ implements Serializable লিখলেই হলো। Serializable একটি marker interface — এর কোনো method নেই, এটা শুধু অনুমতির চিহ্ন।

লেখার কাজ করে ObjectOutputStream-এর writeObject, পড়ার কাজ ObjectInputStream-এর readObject. এই format মানুষের পড়ার মতো নয়; এর প্রথম চারটি byte সবসময় AC ED 00 05 (OpenJDK 21-এ যাচাই করা) — এবার একটি অ্যাপের সেটিং object লেখা আর পড়া দেখুন, যার একটি field transient:

ধাপ ১new Settings("dark", 16, "pin1234")
Settings ─┬─ theme    = "dark"
          ├─ fontSize = 16
          └─ password = "pin1234"   transient
constructor ran   ← constructor চলল, একবার
ধাপ ২out.writeObject(s)
AC ED 00 05                    stream-এর মাথা
class descriptor:
   নাম = "Settings"
   serialVersionUID = 1
   field: fontSize (int), theme (String)   ← password নেই
মান: 16, "dark"
ধাপ ৩byte[] — ফাইল, নেটওয়ার্ক বা মেমরি
byte[] bytes = buf.toByteArray();
এখন এটা শুধু byte — চাইলে ফাইলে রাখা যায়,
সকেটে পাঠানো যায়; এখানে মেমরিতেই রাখা হলো
ধাপ ৪in.readObject()
stream বলে: class "Settings", UID 1
JVM: নিজের Settings class খুঁজল → UID 1 ✔ মিলেছে
Settings-এর constructor ডাকা হলো না
খালি object-এ সরাসরি বসল: fontSize = 16, theme = "dark"
ধাপ ৫ফল
Settings ─┬─ theme    = "dark"
          ├─ fontSize = 16
          └─ password = null   ← ডিফল্ট মান
dark 16 null
ধাপ ৬ObjectInputFilter "java.base/*;!*"
stream বলে: class "Settings"
filter: java.base-এর class? না → "!*" → REJECTED
object তৈরির আগেই থামল
blocked: filter status: REJECTED
  1. Object তৈরি। সাধারণভাবে new দিয়ে object বানানো হলো, তাই constructor চলল আর “constructor ran” ছাপা হলো। password field-এর আগে transient লেখা — অর্থাৎ “এটা serialize কোরো না”।
  2. লেখা। writeObject প্রথমে stream-এর মাথা, তারপর class-এর বর্ণনা (নাম, serialVersionUID, field-এর তালিকা), তারপর মানগুলো লেখে। transient field তালিকাতেই নেই — পাসওয়ার্ড কখনো byte-এ যায় না।
  3. শুধু byte. এখন object-টি একটি byte array. বাস্তবে এটা ফাইল বা নেটওয়ার্কে যেত।
  4. পড়া — constructor ছাড়া। readObject stream থেকে class-এর নাম পড়ে, নিজের classpath-এ সেই class খোঁজে, serialVersionUID মেলায়। তারপর Settings-এর constructor না ডেকেই একটি object বানিয়ে byte থেকে সরাসরি field-এ মান বসায়। constructor-এর কোনো যাচাই এখানে চলে না।
  5. transient-এর ফল। password-এর মান stream-এ ছিল না, তাই সে পায় তার টাইপের ডিফল্ট মান — reference হলে null, int হলে 0, boolean হলে false. আউটপুট dark 16 null.
  6. Filter দিয়ে পাহারা। একই byte এবার ObjectInputFilter লাগিয়ে পড়া হলো। নিয়ম java.base/*;!* মানে “শুধু JDK-র মূল module-এর class, বাকি সব নিষেধ”। Settings নিষিদ্ধ, তাই object তৈরির আগেই InvalidClassException. অংশ ৮-এ দেখবেন, এটা কেন জীবন বাঁচানো অভ্যাস।

এবার চালিয়ে মিলিয়ে নিন। সব কাজ মেমরিতে হচ্ছে, কোনো ফাইল লাগে না:

import java.io.*;

class Settings implements Serializable {
    private static final long serialVersionUID = 1L;
    String theme; int fontSize;
    transient String password;                       // ফাইলে যাবে না
    Settings(String t, int f, String p) { theme = t; fontSize = f; password = p; System.out.println("constructor ran"); }
}

class Main {
    public static void main(String[] args) throws Exception {
        var buf = new ByteArrayOutputStream();
        try (var out = new ObjectOutputStream(buf)) { out.writeObject(new Settings("dark", 16, "pin1234")); }
        byte[] bytes = buf.toByteArray();
        try (var in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            var s = (Settings) in.readObject();
            System.out.println(s.theme + " " + s.fontSize + " " + s.password);
        }
        try (var in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            in.setObjectInputFilter(ObjectInputFilter.Config.createFilter("java.base/*;!*"));
            in.readObject();
        } catch (InvalidClassException e) { System.out.println("blocked: " + e.getMessage()); }
    }
}

আউটপুট তিন লাইন: constructor ran, dark 16 null, আর blocked: filter status: REJECTED. সবচেয়ে গুরুত্বপূর্ণ জিনিসটি হলো যা ছাপা হয়নি: object দুবার পড়া হলেও “constructor ran” এসেছে মাত্র একবার — শুধু new-এর সময়। readObject constructor না ডেকেই object বানিয়েছে। দ্বিতীয় লাইনে theme আর fontSize ফিরে এসেছে, কিন্তু transient password হয়েছে null. তৃতীয় লাইনে filter Settings class-টিকে ঢুকতেই দেয়নি। try (...) হলো পর্ব ২৯-এর try-with-resources; throws Exception শুধু উদাহরণ ছোট রাখতে।

serialVersionUID — class-এর সংস্করণ নম্বর

stepper-এর ধাপ ৪-এ JVM serialVersionUID মিলিয়েছিল। এটা class-এর একটি সংস্করণ নম্বর, যা stream-এ লেখা থাকে। ধরুন অ্যাপের পুরোনো সংস্করণ সেটিং ফাইলে লিখেছিল UID 1 দিয়ে, আর নতুন সংস্করণে কেউ class বদলে UID 2 করলেন। পুরোনো ফাইল পড়তে গেলে (OpenJDK 21-এ যাচাই করা):

java.io.InvalidClassException: Settings; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

অর্থাৎ InvalidClassException: “stream-এর class আর আমার কাছের class এক নয়”। আরও খারাপ হয় যদি আপনি serialVersionUID নিজে না লেখেন। তখন JVM class-এর গঠন (নাম, field, method, interface) থেকে একটি নম্বর হিসাব করে নেয়, আর class-এ একটি method যোগ করলেও সেই নম্বর বদলে যেতে পারে — হঠাৎ সব পুরোনো ফাইল অপাঠ্য। তাই Serializable class-এ সবসময় private static final long serialVersionUID = 1L; লিখুন। javac সাধারণভাবে এ নিয়ে কিছু বলে না; -Xlint:serial দিয়ে compile করলে সতর্ক করে:

Main.java:3: warning: [serial] serializable class Settings has no definition of serialVersionUID

আর Serializable class-এর ভেতরে এমন কোনো field থাকে, যার টাইপ নিজে Serializable নয় (এবং field-টি transient নয়), তাহলে compile ঠিকই হয়, কিন্তু লেখার সময় exception আসে — ধরুন field-এর টাইপ Order:

java.io.NotSerializableException: Order

record-এর ক্ষেত্রে নিয়ম আলাদা। পর্ব ২৫-এর ভাবনার প্রশ্ন ৪-এ দেখেছেন: Serializable record পড়ার সময় JVM canonical constructor ডাকে, তাই compact constructor-এর যাচাই চলে। তবু সবচেয়ে ভালো পছন্দ হলো Java serialization এড়িয়ে চলা — কারণ পরের অংশে।

৮ · বিপদ: অবিশ্বস্ত ডেটা deserialize করা — আর নতুন কোডে কী ব্যবহার করবেন

অংশ ২-এ বলা হয়েছিল, JSON পড়ার সময় কোন টাইপ বানানো হবে, তা ঠিক করে আপনার প্রোগ্রাম। Java serialization-এ ঠিক উল্টো: কোন class-এর object বানানো হবে, তা লেখা থাকে stream-এর ভেতরে। cast চলে object তৈরি হওয়ার পরে — ততক্ষণে যা হওয়ার হয়ে গেছে।

কী হতে পারে? অনেক class-এর ভেতরে বিশেষ method থাকে (যেমন নিজস্ব readObject), যা deserialization-এর সময় নিজে থেকে চলে। classpath-এ থাকা নিরীহ লাইব্রেরিগুলোর এমন কয়েকটি class একটির ভেতরে আরেকটি সাজিয়ে পাঠালে, একটির কাজ আরেকটিকে ডাকে, সেটি আরেকটিকে — শেষে গিয়ে কোনো একটি ক্ষতিকর কাজ ঘটে যায়, এমনকি আক্রমণকারীর পছন্দের command চালানো পর্যন্ত। এই সাজানো শিকলকে বলে gadget chain. এর জন্য আপনার কোডে কোনো bug লাগে না — classpath-এ উপযুক্ত লাইব্রেরি থাকলেই যথেষ্ট।

২০১৫ সালের শুরুতে নিরাপত্তা-গবেষক Chris Frohoff আর Gabriel Lawrence একটি সম্মেলনে দেখান, অতি-প্রচলিত Apache Commons Collections লাইব্রেরির কয়েকটি class দিয়ে এমন শিকল বানানো যায়। একই বছরের নভেম্বরে FoxGlove Security দেখায়, WebLogic, WebSphere, JBoss, Jenkins-এর মতো বহুল ব্যবহৃত সার্ভার-software এই পথে আক্রান্ত হতে পারে — কারণ এরা নেটওয়ার্ক থেকে আসা byte সরাসরি readObject দিয়ে পড়ত। এগুলোর জন্য একাধিক CVE নম্বর দেওয়া হয় (যেমন Oracle WebLogic-এর CVE-2015-4852), আর Commons Collections 3.2.2-তে বিপজ্জনক class-টির deserialization ডিফল্টে বন্ধ করা হয়।

Java-র নকশাকারীরা নিজেরাই এ নিয়ে স্পষ্ট। Java Platform-এর প্রধান স্থপতি Mark Reinhold ২০১৮ সালে একটি সাক্ষাৎকারে ১৯৯৭ সালের এই নকশাকে একটি “ভয়ংকর ভুল” (horrible mistake) বলেছিলেন, আর তাঁর হিসাবে Java-র নিরাপত্তা-ত্রুটির অন্তত এক-তৃতীয়াংশ, হয়তো অর্ধেক, serialization-এর সাথে জড়িত। Brian Goetz ২০১৯ সালের “Towards Better Serialization” লেখায় সমস্যার মূল ধরিয়ে দেন: serialization কার্যত একটি অদৃশ্য কিন্তু public constructor, যা আপনার লেখা constructor-এর সব যাচাই পাশ কাটিয়ে যায় (অংশ ৭-এর runnable কোডে ঠিক এটাই দেখেছেন)। আর Joshua Bloch-এর Effective Java (তৃতীয় সংস্করণ) পরামর্শ দেয়: নতুন কোনো সিস্টেমে Java serialization ব্যবহারের কারণ নেই।

যদি পুরোনো কোডে থাকতেই হয় — ObjectInputFilter

পুরোনো সিস্টেম রাতারাতি বদলানো যায় না। তাই Java 9-এ JEP 290 এনেছে deserialization filter (পরে Java 8u121-সহ পুরোনো সংস্করণের আপডেটেও যোগ হয়েছে), আর Java 17-এ JEP 415 প্রতিটি প্রসঙ্গের জন্য আলাদা filter বেছে নেওয়ার ব্যবস্থা দিয়েছে। মূল নীতি allow-list: যে class-গুলো আসার কথা, শুধু সেগুলোর নাম দিন, বাকি সব !* দিয়ে নিষেধ — অংশ ৭-এর কোডের মতো। filter class-এর নাম দেখে object তৈরির আগেই থামায়। পুরো JVM-এর জন্য jdk.serialFilter system property-ও আছে। শুধু পরিচিত বিপজ্জনক class নিষেধ করা যথেষ্ট নয়, কারণ নতুন শিকল বের হতেই থাকে।

সোজা নিয়ম: নেটওয়ার্ক, ব্যবহারকারীর আপলোড করা ফাইল, cookie, message queue — বাইরের কোনো উৎস থেকে আসা byte কখনো ObjectInputStream.readObject দিয়ে পড়বেন না। নতুন কোডে Java serialization একেবারেই না; ডেটার জন্য JSON বা Protocol Buffers, আর পড়ার দিকে শুধু নিজের ঠিক করা record.

বিকল্পগুলো — কোনটি কখন

JSON-এর দুর্বলতা আকার আর গতি: প্রতিটি object-এ key-এর নাম বারবার লেখা হয়। তাই বিপুল বার্তার সিস্টেমে binary ফরম্যাট চলে — Google-এর Protocol Buffers আর Apache Avro. দুটিই ডেটার গঠন আগে থেকে একটি schema-তে লিখে রাখে, তাই key-এর নাম প্রতিবার পাঠাতে হয় না।

দিকJSONProtocol BuffersAvroJava serialization
মানুষ পড়তে পারে?হ্যাঁনা (binary)না (binary)না (binary)
schemaঐচ্ছিক (JSON Schema)বাধ্যতামূলক, .proto ফাইলবাধ্যতামূলক, JSON-এ লেখাJava class নিজেই
আকারবড় (key বারবার)ছোট (key-এর বদলে field নম্বর)খুব ছোট (schema আলাদা থাকে)বড় (class-এর বর্ণনাসহ)
গতিমাঝারিসাধারণত দ্রুতসাধারণত দ্রুতসাধারণত ধীর
ভাষাপ্রায় সবঅনেক (কোড তৈরি করে)অনেকশুধু Java
সংস্করণ বদলনিজের নিয়মে (অজানা key উপেক্ষা)field নম্বর দিয়ে নিরাপদলেখক-পাঠকের schema মিলিয়েভঙ্গুর (serialVersionUID)
নিরাপত্তাশুধু ডেটাশুধু ডেটাশুধু ডেটাযেকোনো class — ঝুঁকিপূর্ণ
সাধারণ ব্যবহারওয়েব ও মোবাইল API, configgRPC, microserviceKafka, Hadoop-এর বিগ ডেটাপুরোনো RMI, পুরোনো cache

বেশিরভাগ REST API-র জন্য JSON-ই যথেষ্ট, আর debug করা সহজ; ভেতরের service-গুলোর মধ্যে খুব বেশি বার্তা চললে Protocol Buffers. ডেটা কীভাবে সময়ের সাথে বদলায়, তা সামলানোকে বলে schema evolution; ভাবনার প্রশ্ন ৪-এ এর গল্প। আর আধুনিক Java-তে সবচেয়ে প্রচলিত জুটি: record + JSON — record দেয় যাচাই-করা অপরিবর্তনীয় ডেটা, JSON দেয় সবার বোঝার মতো শুধু-ডেটার ফরম্যাট।

পরীক্ষা ও ইন্টারভিউতে

Serialization আর deserialization কী? object-কে byte বা লেখায় রূপ দেওয়া, আর সেখান থেকে আবার object বানানো।
transient field-এর কী হয়? serialize হয় না; পড়ার পর ডিফল্ট মান পায় (null, 0, false).
serialVersionUID কেন লিখতে হয়? class-এর সংস্করণ নির্দিষ্ট রাখতে; না মিললে InvalidClassException, আর না লিখলে JVM-এর হিসাব করা নম্বর ছোট বদলেও পাল্টে যেতে পারে।
Deserialization-এ কি constructor চলে? সাধারণ Serializable class-এর নিজের constructor চলে না; record-এর ক্ষেত্রে canonical constructor চলে।
Jackson-এ generic List কীভাবে পড়বেন? new TypeReference<List<Item>>() {} দিয়ে, কারণ type erasure-এ generic টাইপ মুছে যায়।

৯ · AI-এর যুগে — JSON আর serialization কোড যাচাই

AI-কে “অর্ডারটা JSON বানিয়ে পাঠাও” বললে প্রায়ই আসে String জোড়া দেওয়া কোড — দেখতে চলে, টেস্টে “রনি” দিয়ে পাস করে, আর প্রথম যে গ্রাহক নামের সাথে কোট চিহ্ন লেখেন, তাঁর অর্ডারে ভেঙে পড়ে। আবার “object ফাইলে সেভ করো” বললে অনেক সময় আসে ObjectOutputStream.

AI যা করেকেন সমস্যাআপনি যা করবেন
String জোড়া বা text block দিয়ে JSON বানায়কোট, নতুন লাইন, backslash-এ JSON ভাঙে; injection-এর ঝুঁকিJackson/Gson চান; হাতে লেখা JSON গ্রহণ করবেন না
সংরক্ষণ বা নেটওয়ার্কের জন্য Java serializationনিরাপত্তা-ঝুঁকি, শুধু Java, সংস্করণ বদলে ভাঙেJSON (বা Protocol Buffers) চান
বাইরের byte-এ readObject, কোনো filter নেইgadget chain-এর দরজা খোলাপুরোপুরি বাদ; না পারলে allow-list ObjectInputFilter
অজানা key বা তারিখের কথা ভাবে নাসার্ভার নতুন field দিলেই অ্যাপ ভাঙে; Instant-এ exception বা সংখ্যাignoreUnknown নিয়ে সিদ্ধান্ত, JavaTimeModule, ISO string
টাকা double-এ, বড় ID number হিসেবেদশমিকের ভুল, JavaScript-এ ২৫৩-এর পরে অঙ্ক বদলায়BigDecimal + string, বা পয়সায় long; ID string
parse-এর পর কোনো যাচাই নেইnull আর ঋণাত্মক টাকা নিখুঁতভাবে parse হয়ে ঢোকেrecord-এর compact constructor-এ যাচাই

AI-কে JSON-সংক্রান্ত কোড লিখতে বা রিভিউ করতে বলার সময় এই কাঠামো ব্যবহার করুন:

আমি Java 21 আর Jackson 2.x ব্যবহার করছি। নিচের JSON উত্তরটি পড়ে Java record-এ নেওয়ার কোড লিখে দাও: [এখানে নমুনা JSON] শর্ত: ১. প্রতিটি JSON object-এর জন্য একটি record; snake_case key থাকলে @JsonProperty দিয়ে camelCase নাম দাও। ২. হাতে String জোড়া দিয়ে JSON লিখবে বা indexOf দিয়ে পড়বে না; শুধু ObjectMapper. ৩. টাকার field BigDecimal (JSON-এ string হিসেবে এলেও), সময়ের field Instant বা LocalDate; JavaTimeModule যোগ করো আর WRITE_DATES_AS_TIMESTAMPS বন্ধ রাখো। ৪. অজানা key-এর জন্য কী করবে, তা স্পষ্ট বলো (ignoreUnknown চালু রাখলে কেন)। ৫. প্রতিটি record-এর compact constructor-এ বাধ্যতামূলক field-এর null আর মানের সীমা যাচাই করো; List থাকলে List.copyOf. ৬. top-level array থাকলে TypeReference ব্যবহার করো। ৭. কোথাও ObjectOutputStream/ObjectInputStream বা Serializable ব্যবহার করবে না; করতে হলে কারণ আর allow-list ObjectInputFilter দেখাও। ৮. শেষে তিনটি পরীক্ষা লেখো: একটি বৈধ JSON, একটি অজানা key-ওয়ালা JSON, আর একটি ঋণাত্মক টাকার JSON — প্রতিটিতে কী হওয়া উচিত।

AI-এর লেখা JSON কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Douglas Crockford বলতেন, তিনি JSON “আবিষ্কার” করেছেন, “উদ্ভাবন” নয়। এর মানে কী, আর এত ছোট একটি ফরম্যাট কীভাবে ECMA-404 আর RFC 8259-এর মতো আনুষ্ঠানিক মান হয়ে উঠল?

২০০০ সালের আশপাশে ওয়েব-ব্রাউজার আর সার্ভারের মধ্যে ডেটা আদান-প্রদানের ভাষা ছিল মূলত XML. Douglas Crockford আর তাঁর সহকর্মীরা তখন একটি ছোট কোম্পানিতে (State Software) ব্রাউজারে চলা অ্যাপ বানাচ্ছিলেন। তাঁরা লক্ষ করেন, JavaScript-এর ভেতরেই ডেটা লেখার একটি চমৎকার উপায় আছে — object literal: {name: "Rahim", marks: [80, 92]}. ব্রাউজার এটা নিজেই বোঝে, আলাদা parser লাগে না। তাঁরা এই লেখার ধরন থেকে একটি কঠোর, ছোট উপসেট আলাদা করেন — key সবসময় ডাবল কোটে, শুধু ছয় ধরনের মান, কোনো function বা চলক নয় — আর নাম দেন JSON.

“আবিষ্কার” কথাটার মানে এখানেই: নিয়মগুলো নতুন কিছু নয়, আগে থেকেই একটি জনপ্রিয় ভাষার ভেতরে ছিল। Crockford-এর কাজ ছিল সেগুলো খুঁজে বের করা, নাম দেওয়া, আর লিখে ফেলা। ২০০২ সালের দিকে তিনি json.org নামে একটি ছোট ওয়েবসাইটে পুরো ব্যাকরণ ছবি দিয়ে প্রকাশ করেন — এক পাতায়। এই সরলতাই JSON-এর সবচেয়ে বড় বিক্রয়-কৌশল: যেকোনো ভাষায় একজন প্রোগ্রামার এক বিকেলে একটি JSON parser লিখে ফেলতে পারেন।

আনুষ্ঠানিক মানের পথ: ২০০৬ সালে RFC 4627 (Crockford-এর লেখা, তথ্যমূলক RFC), ২০১৩ সালে Ecma International-এর ECMA-404, ২০১৪ সালে RFC 7159, আর ২০১৭ সালে RFC 8259 — যা এখন Internet Standard. RFC 8259-এর একটি গুরুত্বপূর্ণ সিদ্ধান্ত: সিস্টেমের বাইরে আদান-প্রদানের JSON অবশ্যই UTF-8-এ। বাংলা লেখার জন্য এটা সুখবর।

একটি মজার কাহিনি comment নিয়ে। প্রথম দিকে JSON-এ comment রাখার কথা ভাবা হয়েছিল, কিন্তু Crockford তা বাদ দেন। তাঁর ব্যাখ্যা ছিল, মানুষ comment-এর ভেতরে parser-এর জন্য বিশেষ নির্দেশ লিখতে শুরু করেছিল, যা আলাদা parser-এর মধ্যে মিল নষ্ট করত। আরেকটি সূক্ষ্ম তথ্য: অনেক বছর ধরে JSON পুরোপুরি JavaScript-এর উপসেট ছিল না — দুটি বিশেষ Unicode লাইন-বিভাজক অক্ষর JSON string-এ বৈধ কিন্তু JavaScript string-এ অবৈধ ছিল; ২০১৯ সালের JavaScript মান (ES2019) সেই ফাঁক বন্ধ করে।

মূল উপলব্ধি: যে ফরম্যাট সবচেয়ে কম জিনিস করে, সেটাই সবচেয়ে বেশি জায়গায় টেকে। JSON-এর জয় এসেছে বৈশিষ্ট্যের প্রাচুর্য থেকে নয়, এমন সরলতা থেকে, যা যেকোনো ভাষায় একই অর্থে বাস্তবায়ন করা যায়।

প্র ০২JSON-এর আগে XML ছিল ডেটা আদান-প্রদানের রাজা, বিশেষ করে enterprise Java-তে। XML কেন এত প্রভাবশালী ছিল, আর ওয়েব API-তে JSON কীভাবে তার জায়গা নিল?

XML ১৯৯৮ সালে W3C-র recommendation হয়। এর লক্ষ্য ছিল উচ্চাভিলাষী: যেকোনো ধরনের নথি আর ডেটার জন্য একটি সার্বজনীন, নিজেকে-বর্ণনা-করা ফরম্যাট। XML-এর সাথে এলো একটি পুরো পরিবার — XML Schema দিয়ে গঠন যাচাই, namespace দিয়ে নামের সংঘাত এড়ানো, XSLT দিয়ে রূপান্তর, XPath দিয়ে খোঁজা। ২০০০-এর দশকের শুরুতে enterprise জগৎ এর ওপর বিশাল ব্যবস্থা দাঁড় করায়: SOAP দিয়ে web service, WSDL দিয়ে সেবার বর্ণনা, আর Java-তে JAXB, JAX-WS-এর মতো API. ব্যাংক, বিমা, সরকারি সিস্টেমে আজও এর অনেকটা চলছে।

কিন্তু এই শক্তির দাম ছিল জটিলতা। একটি সরল তথ্য পাঠাতেও XML-এ খোলা আর বন্ধ ট্যাগ লাগে, প্রায়ই SOAP-এর খাম আর namespace-এর লম্বা ঘোষণা। আর প্রোগ্রামের ডেটা কাঠামোর (object, list, map) সাথে XML-এর গাছ সরাসরি মেলে না; মাঝে একটি ভারী mapping স্তর লাগে।

পরিবর্তন আসে ব্রাউজার থেকে। ২০০৫ সালে Jesse James Garrett “Ajax” শব্দটি জনপ্রিয় করেন — পাতা পুনরায় না খুলে পেছনে সার্ভারের সাথে কথা বলা। মজার ব্যাপার, Ajax-এর X ছিল XML-এর, আর ব্রাউজারের API-র নামও XMLHttpRequest. কিন্তু ডেভেলপাররা দেখলেন, JSON পাঠালে JavaScript তা সরাসরি object হিসেবে পায়, XML-এর গাছ ঘাঁটতে হয় না। একই সময়ে Roy Fielding-এর ২০০০ সালের গবেষণাপত্রের REST ধারণা জনপ্রিয় হয় — SOAP-এর ভারী খামের বদলে সাধারণ HTTP আর হালকা বার্তা। REST আর JSON একে অপরের স্বাভাবিক জুটি হয়ে ওঠে; বড় ওয়েব কোম্পানিগুলোর পাবলিক API একে একে JSON-কেন্দ্রিক হয়ে যায়।

XML হারিয়ে যায়নি — শুধু জায়গা বদলেছে। Maven-এর pom.xml, Android-এর layout ফাইল, Microsoft Office-এর .docx (ভেতরে XML), SVG ছবি, ব্যাংকের কিছু বার্তা-মান — নথিধর্মী আর কঠোর schema-নির্ভর জায়গায় XML এখনো শক্ত।

মূল উপলব্ধি: প্রযুক্তি বদলায় যখন দৈনন্দিন কাজের খরচ বদলায়। XML “সব সমস্যার” জন্য নকশা করা ছিল; JSON সবচেয়ে সাধারণ সমস্যাটির জন্য — প্রোগ্রামের ডেটা পাঠানো — আর সেখানেই সে জিতেছে।

প্র ০৩Java serialization ১৯৯৭ সালে JDK 1.1-এ এসেছিল সুবিধা হিসেবে। Mark Reinhold কেন ২০১৮ সালে একে “ভয়ংকর ভুল” বললেন? নকশার কোন সিদ্ধান্ত এর মূল সমস্যা?

১৯৯৭ সালের প্রেক্ষাপট ভাবুন। Java তখন নতুন, আর Sun-এর লক্ষ্য ছিল নেটওয়ার্কে ছড়ানো object-এর জগৎ: এক কম্পিউটারের Java প্রোগ্রাম আরেক কম্পিউটারের object-এর method ডাকবে (RMI), আর JavaBeans উপাদান নিজের অবস্থা ফাইলে রেখে দেবে। এর জন্য দরকার ছিল যেকোনো object graph-কে byte-এ রূপ দেওয়ার উপায় — আর প্রোগ্রামারকে যেন প্রায় কিছুই লিখতে না হয়। সমাধান: implements Serializable লিখলেই JVM নিজে সব field, reference ধরে ধরে পুরো graph লিখে ফেলবে।

সমস্যা লুকিয়ে ছিল এই “নিজে নিজে” কাজের ভেতরে। প্রথমত, deserialization class-এর constructor না ডেকে object বানায় (অংশ ৭-এ নিজে দেখেছেন)। Brian Goetz পরে একে বলেছেন এক ধরনের অদৃশ্য কিন্তু public constructor — ভাষার নিয়মের বাইরে দিয়ে object তৈরির পথ, যেখানে encapsulation আর invariant-এর কোনো পাহারা নেই। দ্বিতীয়ত, কোন class বানানো হবে তা stream ঠিক করে, পাঠক নয়। তৃতীয়ত, class-এর private field-গুলোই format হয়ে যায় — ফলে ভেতরের নকশা বদলানো মানে ফাইল-ফরম্যাট বদলানো।

এই তিনটি মিলে নিরাপত্তার দুঃস্বপ্ন তৈরি হয়। ২০১৫ সালের Apache Commons Collections ঘটনা (অংশ ৮) দেখাল, classpath-এর নিরীহ class জুড়ে জুড়ে দূর থেকে কোড চালানো যায়। এরপর বছরের পর বছর নানা লাইব্রেরি আর সার্ভারে একই ধরনের দুর্বলতা পাওয়া গেছে। ২০১৮ সালে InfoWorld-এর সাথে এক সাক্ষাৎকারে Java Platform-এর প্রধান স্থপতি Mark Reinhold বলেন, serialization ছিল ১৯৯৭ সালের একটি “horrible mistake”, আর তাঁর অনুমান, Java-র নিরাপত্তা-ত্রুটির অন্তত এক-তৃতীয়াংশ, হয়তো অর্ধেক, serialization-এর সাথে জড়িত।

সরানো এখনো সম্ভব হয়নি — কারণ backward compatibility; কোটি লাইনের পুরোনো কোড এর ওপর দাঁড়িয়ে। তাই Java ধাপে ধাপে ক্ষতি কমিয়েছে: Java 9-এ JEP 290 (deserialization filter), Java 17-এ JEP 415 (প্রসঙ্গভিত্তিক filter), আর Java 16-এ record — যার deserialization canonical constructor দিয়ে যায়, অর্থাৎ “অদৃশ্য constructor” সমস্যাটি নেই। Goetz-এর ২০১৯ সালের “Towards Better Serialization” নোটে ভবিষ্যতের দিশা: serialization হবে ভাষার সাধারণ নিয়মের ভেতরে — সবার দেখা যায় এমন (public) constructor আর accessor দিয়ে।

মূল উপলব্ধি: “প্রোগ্রামারকে কিছু লিখতে হবে না” — এই সুবিধার দাম ছিল ভাষার নিয়মের বাইরে একটি গোপন দরজা। নিরাপদ নকশায় প্রতিটি object একটি দৃশ্যমান, যাচাই-করা দরজা দিয়েই জন্মায়।

প্র ০৪Google-এ Protocol Buffers কেন তৈরি হয়েছিল, আর “schema evolution” — পুরোনো ও নতুন সংস্করণের প্রোগ্রাম একসাথে চলা — এটা কীভাবে সামলায়? JSON আর Java serialization-এর সাথে তুলনা করুন।

Google-এর ভেতরে হাজারো সার্ভার একে অপরের সাথে প্রতিনিয়ত বার্তা আদান-প্রদান করে। ২০০০-এর দশকের শুরুতে সেখানে একটি ফরম্যাট তৈরি হয়, যার নাম Protocol Buffers; ২০০৮ সালে Google এটি open source করে। লক্ষ্য তিনটি: বার্তা ছোট হবে (নেটওয়ার্ক আর disk-এর খরচ কম), পড়া-লেখা দ্রুত হবে, আর সবচেয়ে গুরুত্বপূর্ণ — সিস্টেম বদলালেও পুরোনো আর নতুন সংস্করণ একে অপরকে বুঝবে। বড় সিস্টেমে সব সার্ভার একসাথে হালনাগাদ করা যায় না; কিছুদিন পুরোনো আর নতুন কোড পাশাপাশি চলে।

Protocol Buffers-এ ডেটার গঠন লেখা হয় একটি .proto ফাইলে: string trx_id = 1; int64 amount_paisa = 2;. এখানে নম্বরগুলো আসল চাবি — byte-এ key-এর নাম যায় না, যায় এই field নম্বর। এখান থেকেই schema evolution-এর নিয়ম: নতুন field যোগ করুন নতুন নম্বর দিয়ে; পুরোনো প্রোগ্রাম অজানা নম্বর দেখলে সেটা বাদ দিয়ে এগিয়ে যায়। কোনো field বাদ দিলে তার নম্বর আর কখনো অন্য কাজে ব্যবহার করবেন না (reserved দিয়ে আটকে রাখা যায়), কারণ পুরোনো বার্তায় সেই নম্বরের ডেটা ভুল অর্থে পড়া হবে। নতুন প্রোগ্রাম পুরোনো বার্তায় নতুন field না পেলে পায় ডিফল্ট মান।

Apache Avro (Hadoop প্রকল্প থেকে, Doug Cutting-এর হাতে) আরও এক ধাপ এগোয়: byte-এ field নম্বরও যায় না। পড়ার সময় লেখকের schema আর পাঠকের schema পাশাপাশি রেখে মিলিয়ে নেওয়া হয় — তাই বার্তা খুব ছোট, কিন্তু লেখকের schema কোথাও পাওয়া যেতে হবে (Kafka-র জগতে এর জন্য schema registry ব্যবহার হয়)।

তুলনা করুন। JSON-এ key-এর নাম প্রতিটি বার্তায় থাকে, তাই অজানা key উপেক্ষা করা সহজ — Jackson-এর ignoreUnknown ঠিক এই “সহনশীল পাঠক” নীতি। কিন্তু কোনো schema-বাধ্যবাধকতা নেই, তাই কেউ amount-কে string থেকে number বানিয়ে দিলে কেউ আগে থেকে ধরে না। আর Java serialization-এ class-এর private field-ই format; field যোগ বা serialVersionUID বদলের মতো ছোট পরিবর্তনেও InvalidClassException — evolution এখানে সবচেয়ে ভঙ্গুর।

মূল উপলব্ধি: serialization ফরম্যাট বেছে নেওয়া মানে শুধু আজকের আকার-গতি নয়, আগামী পাঁচ বছরে ডেটা কীভাবে বদলাবে তার পরিকল্পনা। স্থির নম্বর বা স্পষ্ট schema ভবিষ্যতের বদলকে নিরাপদ করে।

প্র ০৫JSON-এ টাকার অঙ্ক বা বড় ID সংখ্যা হিসেবে পাঠানো কেন বিপজ্জনক? JavaScript-এর ২৫৩ সীমা কী, আর বাস্তবে কোম্পানিগুলো কীভাবে এর মুখোমুখি হয়েছে?

JSON-এর ব্যাকরণ শুধু বলে সংখ্যা কেমন দেখতে হবে; সংখ্যাটা পড়ার পর কোন টাইপে রাখা হবে, তা বলে না। RFC 8259 নিজেই সতর্ক করে: বাস্তবে বহু প্রোগ্রাম সংখ্যাকে IEEE 754 double হিসেবে রাখে, তাই আদান-প্রদান নিরাপদ রাখতে চাইলে সংখ্যা সেই সীমার ভেতরে রাখা ভালো। JavaScript-এ সংখ্যার একটিমাত্র সাধারণ টাইপ — Number — যা double. double-এ পূর্ণসংখ্যা নির্ভুলভাবে রাখা যায় শুধু ২৫৩ − ১ (9007199254740991, JavaScript-এ Number.MAX_SAFE_INTEGER) পর্যন্ত। এর ওপরে গেলে পাশাপাশি দুটি পূর্ণসংখ্যার একটি আর আলাদাভাবে রাখা যায় না; ব্রাউজারের JSON.parse নীরবে কাছের রাখা-যায় এমন মানে বদলে দেয়, কোনো error ছাড়াই।

Java-র long ২৬৩ − ১ পর্যন্ত যায়, তাই Java সার্ভার নিশ্চিন্তে একটি বড় ID পাঠায় — আর ওয়েব পাতায় সেটা অন্য ID হয়ে যায়। এর বিখ্যাত উদাহরণ Twitter: ২০১০ সালের দিকে তারা নতুন ID-ব্যবস্থা (Snowflake) চালু করে, যার ID ২৫৩ ছাড়িয়ে যায়। তাই API-তে প্রতিটি সংখ্যার id-এর পাশে string হিসেবে id_str যোগ করা হয়, আর ডেভেলপারদের সেটাই ব্যবহার করতে বলা হয়।

টাকার সমস্যা আরও সূক্ষ্ম। double দশমিক ভগ্নাংশ (০.১, ০.২) নির্ভুলভাবে রাখতে পারে না — JavaScript-এ 0.1 + 0.2 হয় 0.30000000000000004. Java-তে আপনি সযত্নে BigDecimal("150.10") রাখলেন, কিন্তু JSON-এ number হিসেবে পাঠালে পড়ার দিকের double সেটাকে কাছাকাছি মানে রাখবে; লাখো লেনদেন যোগ করলে পয়সার গরমিল দেখা দেয়, আর হিসাব-নিকাশে এটা অগ্রহণযোগ্য। সাধারণ দুটি সমাধান: string হিসেবে পাঠানো ("150.10"), যাতে পাঠক নিজে নির্ভুল টাইপে নেয়; অথবা সবচেয়ে ছোট এককে পূর্ণসংখ্যা (15010 পয়সা), যা double-এর নিরাপদ সীমার মধ্যে নির্ভুল থাকে। বহু পেমেন্ট API দ্বিতীয় পথ নেয়।

JavaScript পরে BigInt টাইপ এনেছে, কিন্তু সাধারণ JSON.parse নিজে থেকে BigInt বানায় না — তাই নিয়মটা এখনো একই।

মূল উপলব্ধি: JSON একটি চুক্তি, আর চুক্তির দুর্বলতম পাঠকের সীমাই আপনার সীমা। যে সংখ্যার প্রতিটি অঙ্ক গুরুত্বপূর্ণ — টাকা, বড় ID — তাকে সংখ্যা নয়, লেখা বা ছোট এককের পূর্ণসংখ্যা হিসেবে পাঠান।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।

  1. একটি case মুছে দেখুন: অংশ ৪-এর চালানো যায় এমন প্রোগ্রামে escaper থেকে case '\n'-এর লাইনটি মুছে দিন। না চালিয়ে বলুন শেষ লাইনের JSON বদলাবে কি না, আর সেটা তখনো বৈধ JSON থাকবে কি না। তারপর চালিয়ে মিলিয়ে নিন।

    বদলাবে, কিন্তু JSON বৈধই থাকবে। নতুন লাইনের অক্ষরের code 10, যা 0x20-এর চেয়ে ছোট — তাই এটা এখন default শাখায় পড়বে আর "\\u%04x".formatted(10) লিখবে \u000a. নামটি দাঁড়াবে "রনি \"R\"\u000aমিয়া". JSON-এ \n আর \u000a একই অক্ষর বোঝায়, তাই যেকোনো parser আসল নামই ফেরত দেবে। অর্থাৎ আসল নিরাপত্তা দেয় “সব নিয়ন্ত্রণ-অক্ষর escape” নিয়মটি। আর case '"' মুছলে? তখন কোট (code 34) default-এ গিয়ে হুবহু বসবে, আর JSON আবার ভাঙবে।

  2. transient আর Serializable সরিয়ে দেখুন: অংশ ৭-এর প্রোগ্রামে (ক) শুধু transient শব্দটি মুছুন; (খ) তারপর implements Serializable মুছুন। প্রতিবার আউটপুট কী হবে, আগে অনুমান করুন।

    (ক) দ্বিতীয় লাইন হবে dark 16 pin1234 — পাসওয়ার্ড এখন stream-এ লেখা হয়েছে আর ফিরে এসেছে। অর্থাৎ ফাইল বা নেটওয়ার্কে পাসওয়ার্ড চলে গেছে, যেটা ঠিক যা আপনি চাননি। (খ) writeObject-এর সময়ই java.io.NotSerializableException: Settings exception আসবে। প্রোগ্রাম এটা ধরে না (শুধু InvalidClassException ধরা হয়েছে), তাই stack trace দেখিয়ে থেমে যাবে — “constructor ran” ছাপার পরেই। compile কিন্তু ঠিকই হবে: Serializable কি না, তা writeObject(Object) চলার সময় দেখে।

  3. এই JSON-এর ভুল খুঁজুন: একজন সহকর্মী হাতে একটি config লিখেছেন — { 'theme': "dark", "fontSize": 16, // বড় লেখা নতুন লাইনে "ratio": NaN, "userId": 9007199254740993, }. কয়টি সমস্যা আছে, আর প্রতিটির সমাধান কী?

    পাঁচটি। (১) 'theme' — একক কোট চলে না, "theme" লিখুন। (২) // বড় লেখা — JSON-এ comment নেই; দরকার হলে আলাদা একটি key ("_note") বা অন্য ফরম্যাট। (৩) NaN — JSON number-এ NaN বা Infinity নেই; null দিন বা বাদ দিন। (৪) 9007199254740993 — ব্যাকরণে বৈধ, কিন্তু ২৫৩-এর চেয়ে বড়; JavaScript-এর মতো double-নির্ভর পাঠক এটাকে 9007199254740992 পড়বে। ID string হিসেবে পাঠান। (৫) শেষের , — trailing comma চলে না। চতুর্থটি সবচেয়ে বিপজ্জনক, কারণ কোনো error ছাড়াই ভুল মান তৈরি করে।

  4. নিজে লিখুন — আবহাওয়ার record: অংশ ২-এর weather.json-এর জন্য Jackson-উপযোগী record লিখুন। temperature_c, rain_expected, max_c-এর মতো key-এর জন্য Java-র camelCase নাম রাখুন, তারিখ LocalDate/OffsetDateTime-এ নিন, আর যাচাই দিন: city খালি নয়, humidity ০–১০০, forecast null নয়।

    একটি সম্ভাব্য সমাধান:

    @JsonIgnoreProperties(ignoreUnknown = true)
    record DayForecast(LocalDate day,
                       @JsonProperty("max_c") int maxC,
                       @JsonProperty("min_c") int minC,
                       String summary) {}
    
    @JsonIgnoreProperties(ignoreUnknown = true)
    record Weather(String city,
                   OffsetDateTime updated,
                   @JsonProperty("temperature_c") double temperatureC,
                   int humidity,
                   @JsonProperty("rain_expected") boolean rainExpected,
                   String alert,                      // null হতে পারে
                   List<DayForecast> forecast) {
        Weather {
            if (city == null || city.isBlank()) throw new IllegalArgumentException("city missing");
            if (humidity < 0 || humidity > 100) throw new IllegalArgumentException("bad humidity: " + humidity);
            forecast = List.copyOf(Objects.requireNonNull(forecast, "forecast missing"));
        }
    }
    // পড়া: mapper (JavaTimeModule-সহ).readValue(text, Weather.class)

    লক্ষ করুন: alert null হতে পারে বলে String রাখা হয়েছে, আর তার কোনো যাচাই নেই। "updated"-এ সময়ের সাথে +06:00 offset আছে, তাই OffsetDateTime (পর্ব ৩৮)। List.copyOf একসাথে defensive copy নেয় আর ভেতরে null থাকলে আটকায়। JSON-এ humidity না থাকলে Jackson primitive int-এ 0 বসাবে — যা যাচাই পার হয়ে যাবে; এটা আটকাতে চাইলে Integer humidity নিয়ে null যাচাই করুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমJackson-এর mapper.readValue(json, Txn.class) চললে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📨 JSON লেখা (UTF-8 byte) পড়া হয়
  2. ✂️ parser লেখাকে token-এ ভাঙে আর ব্যাকরণ যাচাই করে
  3. 🔗 প্রতিটি key-কে record-এর একটি component-এর সাথে মেলানো হয়
  4. 🔄 মানগুলো component-এর টাইপে (BigDecimal, Instant, enum) রূপ নেয়
  5. ✅ canonical constructor চলে, যাচাই পার হলে record ফেরত আসে

নিচের কোনটি বৈধ JSON?

  • {'name': 'Rahim'}
  • {"name": "Rahim",}
  • {"name": "Rahim", "age": 25}
  • {name: "Rahim"}

একটি Serializable class-এ transient String password field আছে। ObjectOutputStream দিয়ে লিখে ObjectInputStream দিয়ে পড়ার পর password-এর মান কী হবে?

  • আগের মানটাই থাকবে
  • null
  • "" (খালি String)
  • NotSerializableException হবে

একটি class-এর serialVersionUID 1 থাকা অবস্থায় object ফাইলে লেখা হলো। পরে class-এ serialVersionUID 2 করে সেই ফাইল পড়লে কী হবে?

  • ফাইল ঠিকঠাক পড়া যাবে
  • compile error হবে
  • শুধু নতুন field-গুলো ডিফল্ট মান পাবে
  • InvalidClassException হবে

Java সার্ভার থেকে JSON-এ টাকার অঙ্ক পাঠানোর সবচেয়ে নিরাপদ উপায় কোনটি?

  • string হিসেবে ("150.50") বা পয়সায় পূর্ণসংখ্যা (15050)
  • double-এ রেখে number হিসেবে
  • float-এ রেখে number হিসেবে
  • Java serialization-এর byte হিসেবে

Jackson 2.x-এর ডিফল্ট ObjectMapper দিয়ে একটি record পড়ার সময় JSON-এ এমন একটি key এলো, যার সাথে মেলে এমন কোনো component নেই। কী হবে?

  • key-টি নীরবে উপেক্ষিত হবে
  • record-এ নতুন component যোগ হবে
  • UnrecognizedPropertyException হবে
  • সব component null হয়ে যাবে

নিচের কোনটি নিরাপত্তার দিক থেকে সবচেয়ে বিপজ্জনক?

  • ইন্টারনেট থেকে আসা JSON Jackson দিয়ে নিজের record-এ পড়া
  • ইন্টারনেট থেকে আসা byte ObjectInputStream.readObject দিয়ে পড়া
  • নিজের record Jackson দিয়ে JSON-এ লেখা
  • নিজের ফাইল allow-list ObjectInputFilter দিয়ে পড়া

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Serialization = মেমরির object graph-কে লেখা বা byte-এ রূপ দেওয়া; deserialization = সেখান থেকে নতুন object. ফাইলে রাখা আর অন্য প্রোগ্রামে পাঠানোর জন্য দরকার।
  2. JSON-এ ছয় ধরনের মান: object, array, string, number, true/false, null. record → object, List → array, তারিখ → ISO-8601 string, টাকা → string বা পয়সায় পূর্ণসংখ্যা।
  3. হাতে JSON জোড়া দেওয়া ভঙ্গুর — কোট, backslash আর নিয়ন্ত্রণ-অক্ষর escape করতে হয়। বাস্তবে Jackson (writeValueAsString, readValue, TypeReference) বা Gson.
  4. Parse সফল মানে ডেটা সঠিক নয়: অজানা key আর তারিখ নিয়ে সচেতন সিদ্ধান্ত নিন, আর record-এর compact constructor-এ যাচাই রাখুন।
  5. Java serialization: Serializable, transient (ডিফল্ট মানে ফেরে), serialVersionUID (অমিলে InvalidClassException). constructor চলে না, stream ঠিক করে class — অবিশ্বস্ত ডেটায় কখনো নয়; পুরোনো কোডে allow-list ObjectInputFilter.

পরের পর্বে: এতক্ষণ আপনার প্রোগ্রাম একবারে একটি কাজ করেছে। কিন্তু সার্ভার একসাথে শত শত অ্যাপের JSON request নেয় — একটির জন্য অন্যগুলো বসে থাকে না। একাধিক কাজ একসাথে চালানো, আর দুটি thread একই ডেটা ছুঁলে কী বিপদ হয় ও কীভাবে আটকাবেন — thread আর synchronization.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান