ছয় টাইপ, সংখ্যার নির্ভুলতা ও এনকোডিংয়ের ফাঁদ
সংক্ষেপে: JSON-এ মাত্র ছয়টি টাইপ, কিন্তু প্রতিটিই নীরবে আপনার ডেটার আকার বদলে দিতে পারে। এই নিবন্ধে সংখ্যা, স্ট্রিং ও null-এর ফাঁদ একসঙ্গে আলোচনা করা হয়েছে।

মূল কথা: আগে যাচাই করুন ID বা টাকার ফিল্ড 2^53 - 1 ছাড়ায় কি না বা বহু দশমিক অঙ্কের নির্ভুলতা দরকার কি না, তারপর দেখুন স্ট্রিংয়ের সীমা কোড পয়েন্ট গণনা করে নাকি UTF-16 ইউনিট; নিরাপদ ইন্টিজার রেঞ্জের বাইরের যেকোনো মান স্ট্রিং হিসেবে বহন করুন, পার্সার নির্ভুলতা রক্ষা করবে বলে ভরসা করবেন না।
JSON-এ টাইপ মানুষের ধারণার চেয়ে কম, কিন্তু ফাঁদ বেশি: সংখ্যা সরে যায়, স্ট্রিং ভাঙে, আর null আর অনুপস্থিত কি এক জিনিস নয়।
১. টাইপের চিত্র
| টাইপ | লিটেরাল উদাহরণ | JSON-এ বাধ্যবাধকতা | সাধারণ ম্যাপিং |
|---|---|---|---|
| অবজেক্ট | {"a": 1} |
কি অবশ্যই স্ট্রিং, ভ্যালু যেকোনো কিছু | Object / Map / struct |
| অ্যারে | [1, "a", null] |
উপাদান ভিন্ন ভিন্ন টাইপের হতে পারে | Array / List / slice |
| স্ট্রিং | "text" |
UTF-8, ডাবল কোট | String |
| সংখ্যা | 1, -1.5, 1e9 |
শুরুতে শূন্য নেই, + নেই, NaN নেই |
double / Decimal |
| বুলিয়ান | true / false |
ছোট হাতের অক্ষর | bool |
| null | null |
একমাত্র null ভ্যালু | null / Option::None |
দুটি জিনিস নেই, খেয়াল করুন: তারিখ টাইপ নেই, আর ইন্টিজার টাইপ নেই। সব তারিখ স্ট্রিং, আর সব ইন্টিজার ও ফ্লোটিং-পয়েন্ট মান একই "সংখ্যা" টাইপ শেয়ার করে।
২. সংখ্যা: ডাবল প্রিসিশনই ডিফল্ট উত্তর
২.১ নিরাপদ ইন্টিজারের সীমা
IEEE 754 ডাবল প্রিসিশন যে সবচেয়ে বড় ইন্টিজার নিখুঁতভাবে ধারণ করতে পারে তা হলো 2^53 - 1 = 9007199254740991। এর পরে পাশাপাশি ইন্টিজারের মধ্যে ফাঁক দেখা দেয়:
JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992
JSON.stringify({ id: 9007199254740993 })
// '{"id":9007199254740992}'
এটি JSON-এর সমস্যা নয়; এটি "টেক্সট → ডাবল প্রিসিশন" ধাপের সমস্যা। মাঝখানে JavaScript থাকা যেকোনো পাইপলাইন (গেটওয়ে, BFF, ফ্রন্টএন্ড, Node সার্ভিস) এটি হিট করবে।
সমাধান (আমি যতটা সুপারিশ করি সেই ক্রমে):
- পুরো পাইপলাইনে ID-র জন্য স্ট্রিং ব্যবহার করুন:
{"id": "9007199254740993"}, সবচেয়ে কম ঝামেলা; - সার্ভারে উচ্চ-নির্ভুল টাইপ দিয়ে পার্স করুন: Java-র
BigInteger/BigDecimal, Go-রjson.Number, Python-এরdecimal.Decimal; - ফ্রন্টএন্ডে
JSON.parse-এর reviver বা বড় সংখ্যা সমর্থনকারী পার্সার (যেমনjson-bigint) দিয়ে ফিল্ডকেBigIntবানান, তবে মনে রাখুনBigIntসরাসরিJSON.stringify-কে দেওয়া যায় না।
২.২ দশমিক নির্ভুল মান নয়
0.1 + 0.2 // 0.30000000000000004
JSON.stringify(0.1 + 0.2)
// "0.30000000000000004"
টাকার পরিমাণ সবসময় ক্ষুদ্রতম মুদ্রা এককে (সেন্ট) ইন্টিজার হিসেবে বা স্ট্রিং হিসেবে রাখুন; কখনো ফ্লোটিং পয়েন্ট দিয়ে জমা করবেন না।
২.৩ সিরিয়ালাইজেশনের সময় সংখ্যার ফাঁদ
JSON.stringify({ a: 1e21 }) // {"a":1e+21}
JSON.stringify({ a: -0 }) // {"a":0}
JSON.stringify({ a: Infinity }) // {"a":null}
JSON.stringify({ a: NaN }) // {"a":null}
JSON.stringify({ a: 10n }) // TypeError: Do not know how to serialize a BigInt
Infinity ও NaN নীরবে null হয়ে যায়, যা "ডেটা হঠাৎ উধাও" ধরনের সমস্যার মধ্যে সবচেয়ে কঠিন।
২.৪ ভাষাভিত্তিক ডিফল্ট আচরণ
| ভাষা | ডিফল্ট সংখ্যা টাইপ | স্পষ্টভাবে হ্যান্ডল করতে হয় এমন কেস |
|---|---|---|
| JavaScript | double | বড় ইন্টিজার, BigInt সিরিয়ালাইজেশন |
| Go | float64 (encoding/json) |
নির্ভুলতা রাখতে json.Number বা Decoder.UseNumber() ব্যবহার করুন |
| Java (Jackson) | লিটেরাল থেকে অনুমিত int/long/double/BigDecimal |
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS বাধ্যতামূলক করুন |
| Python | int (অসীম নির্ভুলতা) / float |
দশমিক নির্ভুলতা রাখতে parse_float=Decimal ব্যবহার করুন |
| Rust (serde_json) | i64/u64/f64 |
অসীম নির্ভুলতার জন্য arbitrary_precision চালু করতে হয় |
৩. স্ট্রিং: UTF-16 দিয়ে দেখা UTF-8 বাইট-স্ট্রিম
৩.১ দৈর্ঘ্য আর অক্ষর-সংখ্যা এক নয়
JavaScript-এ "🎉".length হলো 2, কারণ ইমোজি সাপ্লিমেন্টারি প্লেনে থাকে এবং সারোগেট পেয়ার দিয়ে প্রকাশ পায়।
সার্ভার যদি "অক্ষর-সংখ্যা" দিয়ে দৈর্ঘ্য সীমাবদ্ধ করে আর ফ্রন্টএন্ড length দিয়ে যাচাই করে, দুই পক্ষের মতানৈক্য নিশ্চিত।
'🎉'.length // 2
Array.from('🎉').length // 1
[...'👨👩👧'].length // 5 (জিরো-উইডথ জয়নারসহ)
কঠোর গণনা দরকার হলে Intl.Segmenter (গ্রাফিম ক্লাস্টার অনুযায়ী) ব্যবহার করুন বা ব্যাকএন্ডে Unicode কোড পয়েন্ট গণনা করুন, এবং API ডকুমেন্টেশনে সংজ্ঞাটি স্পষ্ট করুন।
৩.২ একা সারোগেট
"\ud800"-এর মতো একা হাই সারোগেট ব্যাকরণগতভাবে বৈধ JSON (\uXXXX যেকোনো চারটি হেক্স ডিজিট গ্রহণ করে), কিন্তু এটি বৈধ Unicode টেক্সট নয়।
ES2019 থেকে JSON.stringify একা সারোগেটকে \ud800 হিসেবে এস্কেপ করে, ফলে আউটপুট "সু-গঠিত" হয়।
ভাষা-থেকে-ভাষা স্থানান্তরে অন্য পক্ষ কঠোরভাবে Unicode যাচাই করলে এ ধরনের মান প্রত্যাখ্যাত হবে।
৩.৩ \u2028 ও \u2029
লাইন সেপারেটর ও প্যারাগ্রাফ সেপারেটর JSON-এ বৈধ, কিন্তু সেগুলো সরাসরি JavaScript সোর্স বা <script> ট্যাগে বসালে সিনট্যাক্স এরর হয়।
HTML-এ JSON ইনলাইন করার সময় সবসময় এস্কেপ করুন, বা JSON.parse দিয়ে স্ট্রিং থেকে পার্স করুন।
৩.৪ চীনা লেখা এলোমেলো হওয়ার তিনটি উৎস
- ফাইলটি আসলে GBK আর UTF-8 হিসেবে পড়া হচ্ছে;
- রেসপন্স হেডার
charset=gbkঘোষণা করছে, কিন্তু বিষয়বস্তু আসলে UTF-8; - এটি এমন মিডলওয়্যার পার হচ্ছে যা "স্ট্রিংয়ে ডিকোড করে আবার এনকোড করে", ফলে চীনা লেখা দ্বিতীয়বার ট্রান্সকোড হয়।
তদন্তের ক্রম: আগে বাইট দেখুন (xxd), তারপর রেসপন্স হেডার, শেষে মিডলওয়্যার।
৪. null বনাম "এই কি-টি নেই"
JSON-এ এ দুটি আলাদা:
{"nickname": null}
{}
- প্রথমটি বোঝায় "ফিল্ডটি আছে, মান খালি";
- দ্বিতীয়টি বোঝায় "ফিল্ডটি নেই"।
TypeScript-এ এটি string | null ও nickname?: string-এর পার্থক্য হয়; Java-তে "স্পষ্ট null" ও "ডিফল্ট মান"-এর পার্থক্য।
সম্মত নিয়ম না থাকলে এই ফাটল দেখা দেয়:
// ফ্রন্টএন্ড A-র পদ্ধতি
if (data.nickname === undefined) { /* ডাকনাম নেই ধরে নিন */ }
// ফ্রন্টএন্ড B-র পদ্ধতি
if (!data.nickname) { /* null ও খালি স্ট্রিং একসঙ্গে হ্যান্ডল করুন */ }
সুপারিশ: API ডকুমেন্টেশনে ঘোষণা করুন "মান অনুপস্থিত হলে সমভাবে null ব্যবহার করুন, কি কখনো বাদ দেবেন না", বা এর বিপরীত — তবে একটিই বেছে নিন;
API আপডেটের সময় PATCH সেমান্টিক্স ব্যবহার করুন, যাতে "পাঠানো হয়নি, নাকি null পাঠানো হয়েছে" এই দ্ব্যর্থতা না থাকে।
৫. অ্যারে ও অবজেক্ট
৫.১ অ্যারে "সমজাতীয়" নয়
[1, "two", {"three": 3}, null]
ব্যাকরণগতভাবে বৈধ, কিন্তু টাইপ ডেফিনিশন লেখা খুব কঠিন হয়ে যায়। API-তে অ্যারের উপাদান সমজাতীয় রাখার চেষ্টা করুন; ভিন্ন ধরনের ক্ষেত্রে type ডিসক্রিমিনেন্ট ফিল্ডযুক্ত অবজেক্টের অ্যারে ব্যবহার করুন।
৫.২ অবজেক্টের কি নির্দিষ্ট ক্রমহীন, তবু অনেকে ক্রমের উপর নির্ভর করে
স্পেসিফিকেশন বলে অবজেক্ট হলো "নাম/মান জোড়ার ক্রমহীন সংগ্রহ"। বাস্তবে V8 ইনসার্শন ক্রম সংরক্ষণ করে (ইন্টিজার কি ছাড়া, যেগুলো সাজিয়ে দেওয়া হয়)। কি-এর ক্রমের উপর লজিক (যেমন "প্রথম কি" নেওয়া) তৈরি করা মানে ইমপ্লিমেন্টেশনের একটি খুঁটিনাটিের উপর নির্ভর করা।
৫.৩ স্পার্স অ্যারে ও হোল নেই
JavaScript-এর [1, , 3] সিরিয়ালাইজ হয়ে [1,null,3] হয় — হোল null হয়ে যায়। JSON-এ "স্লট"-এর ধারণা নেই।
৬. কাজে লাগানো যায় এমন চেকলিস্ট
- [ ] API চুক্তিতে সব ID, টাকার পরিমাণ ও অতিবৃহৎ ইন্টিজার স্পষ্টভাবে স্ট্রিং;
- [ ] API ডকুমেন্টেশনে
nullবনাম বাদ দেওয়া ফিল্ডের অর্থ লেখা আছে; - [ ] স্ট্রিং দৈর্ঘ্যের সীমা উল্লেখ করে কোড পয়েন্ট নাকি UTF-16 ইউনিট গণনা করে;
- [ ] সিরিয়ালাইজেশনের আগে
Infinity/NaN/undefinedছেঁকে ফেলুন, বা স্পষ্টভাবেnullকরুন; - [ ] HTML-এ ইনলাইন করা JSON এস্কেপ করা হয়েছে;
- [ ] ফ্রন্টএন্ড ও ব্যাকএন্ড — দুই পক্ষেই "বড় ইন্টিজার + ইমোজি + null" কভার করা একটি কন্ট্রাক্ট টেস্ট কেস আছে।
সাধারণ প্রশ্ন
2^53 - 1-এর উপরের ইন্টিজার কীভাবে হ্যান্ডল করব?
সবচেয়ে কম ঝামেলার উপায় হলো ID-র মতো ফিল্ড পুরো পাইপলাইনে স্ট্রিং হিসেবে পাঠানো, যেমন {"id": "9007199254740993"}। সার্ভারে Java-র BigInteger / BigDecimal, Go-র json.Number বা Python-এর decimal.Decimal দিয়ে নির্ভুলতা রাখা যায়। ফ্রন্টএন্ডে json-bigint-এর মতো পার্সার ফিল্ডকে BigInt বানাতে পারে, তবে মনে রাখবেন BigInt সরাসরি JSON.stringify-কে দেওয়া যায় না।
সিরিয়ালাইজেশনের পর Infinity ও NaN কেন null হয়ে যায়?
JSON-এর সংখ্যা টাইপ NaN ও Infinity-এর মতো অ-স্ট্যান্ডার্ড লিটেরাল গ্রহণ করে না, তাই JSON.stringify কোনো এরর ছাড়াই নীরবে সেগুলোকে null করে দেয়। এটি "ডেটা হঠাৎ উধাও" ধরনের সমস্যার মধ্যে সবচেয়ে কঠিন। সিরিয়ালাইজেশনের আগে স্পষ্টভাবে এ মানগুলো ছেঁকে ফেলুন, বা আপনার ব্যবসায়িক নিয়ম অনুযায়ী null করে লগ করুন।
null পাঠাব, নাকি ফিল্ডটি একেবারে বাদ দেব?
দুটোই কাজ করে, তবে API ডকুমেন্টেশনকে একটি বেছে নিয়ে স্পষ্টভাবে লিখতে হবে, নাহলে প্রতিটি ফ্রন্টএন্ড আলাদা ধারণা করে বসবে। উদাহরণে {"nickname": null} আর {} যথাক্রমে TypeScript-এ string | null ও nickname?: string-এ ম্যাপ করে। API আপডেটের সময় PATCH সেমান্টিক্স ব্যবহার করলে "পাঠানো হয়নি, নাকি null পাঠানো হয়েছে" দ্ব্যর্থতা এড়ানো যায়।