الأنواع الستة ودقة الأعداد ومزالق الترميز
الخلاصة: لا يمتلك JSON سوى ستة أنواع، لكن كل نوع منها قادر على إعادة تشكيل بياناتك بصمت. يغطي هذا المقال مزالق الأعداد والنصوص وnull دفعة واحدة.

الخلاصة: تحقق أولًا مما إذا كانت حقول المعرّفات أو المبالغ النقدية تتجاوز 2^53 - 1 أو تحتاج إلى دقة عشرية كثيرة، ثم تحقق مما إذا كانت حدود النصوص تُحسب بنقاط الشيفرة أم بوحدات UTF-16؛ واحمل أي قيمة خارج نطاق الأعداد الصحيحة الآمنة كنص بدلًا من الوثوق بالمحلل في الحفاظ على الدقة.
يمتلك JSON أنواعًا أقل مما يظن معظم الناس، ومزالق أكثر: الأعداد تنزاح، والنصوص تنكسر، وnull شيء مختلف عن مفتاح غائب.
1. خريطة الأنواع
| النوع | مثال حرفي | القيد في JSON | التحويل الشائع |
|---|---|---|---|
| Object | {"a": 1} |
المفاتيح نصوص، والقيم يمكن أن تكون أي شيء | Object / Map / struct |
| Array | [1, "a", null] |
يمكن أن تكون العناصر غير متجانسة | Array / List / slice |
| String | "text" |
UTF-8، علامات اقتباس مزدوجة | String |
| Number | 1، -1.5، 1e9 |
لا أصفار بادئة، ولا +، ولا NaN |
double / Decimal |
| Boolean | true / false |
بحروف صغيرة | bool |
| null | null |
القيمة الوحيدة من نوع null | null / Option::None |
لاحظ أمرين غير موجودين: لا يوجد نوع للتواريخ، ولا يوجد نوع للأعداد الصحيحة. كل التواريخ نصوص، وكل الأعداد الصحيحة والعشرية تتقاسم نوع "number" الواحد.
2. الأعداد: الدقة المزدوجة هي الإجابة الافتراضية
2.1 حد العدد الصحيح الآمن
أكبر عدد صحيح يمكن أن تمثله دقة الفاصلة العائمة المزدوجة IEEE 754 بدقة تامة هو 2^53 - 1 = 9007199254740991. وما بعد ذلك تظهر فجوات بين الأعداد الصحيحة المتجاورة:
JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992
JSON.stringify({ id: 9007199254740993 })
// '{"id":9007199254740992}'
هذه ليست مشكلة JSON؛ بل مشكلة خطوة "النص → الدقة المزدوجة". وأي مسار يحتوي JavaScript في المنتصف (بوابة، BFF، واجهة أمامية، خدمة Node) سيواجهها.
العلاجات (مرتبة حسب مقدار توصيتي بها):
- استخدم النصوص للمعرّفات على امتداد المسار كله:
{"id": "9007199254740993"}، وهو الأقل إزعاجًا على الإطلاق؛ - حلّل بأنواع عالية الدقة على الخادم:
BigInteger/BigDecimalفي Java، وjson.Numberفي Go، وdecimal.Decimalفي Python؛ - في الواجهة الأمامية، استخدم دالة reviver في
JSON.parseأو محللًا يدعم الأعداد الكبيرة (مثلjson-bigint) لتحويل الحقل إلىBigInt، لكن انتبه إلى أنBigIntلا يمكن تمريره مباشرة إلىJSON.stringify.
2.2 الكسور العشرية ليست قيمًا دقيقة
0.1 + 0.2 // 0.30000000000000004
JSON.stringify(0.1 + 0.2)
// "0.30000000000000004"
مثّل المبالغ النقدية دائمًا كأعداد صحيحة بأصغر وحدة نقدية (السنت) أو كنصوص؛ ولا تجمعها أبدًا بالفاصلة العائمة.
2.3 مزالق الأعداد أثناء التسلسل
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 بصمت، وهذه أصعب فئة من "البيانات اختفت فجأة" يمكن تتبعها.
2.4 السلوك الافتراضي حسب اللغة
| اللغة | نوع الأعداد الافتراضي | الحالات التي تحتاج معالجة صريحة |
|---|---|---|
| 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 |
3. النصوص: تدفق بايتات UTF-8 يُنظر إليه عبر UTF-16
3.1 الطول ليس هو عدد الأحرف
في JavaScript، قيمة "🎉".length هي 2، لأن الإيموجي تعيش في المستويات الإضافية وتُمثَّل بزوج بدائل.
إذا حدّ الخادم الطول بعدد "الأحرف" بينما تحققت الواجهة الأمامية عبر length، فسيكون الطرفان على خلاف مؤكد.
'🎉'.length // 2
Array.from('🎉').length // 1
[...'👨👩👧'].length // 5 (including zero-width joiners)
عندما تحتاج إلى عدّ صارم، استخدم Intl.Segmenter (حسب عنقود الغرافيم) أو احسب نقاط شيفرة Unicode في الخلفية، واذكر التعريف بوضوح في وثائق API.
3.2 البدائل المنفردة
البديل الأعلى المنفرد مثل "\ud800" صالح نحويًا في JSON (\uXXXX يقبل أي أربعة أرقام ست عشرية)، لكنه ليس نص Unicode صالحًا.
منذ ES2019 يهرّب JSON.stringify البدائل المنفردة بالشكل \ud800، بما يضمن أن يكون الناتج "حسن التكوين".
وعند النقل بين اللغات، ستُرفض قيم كهذه إذا كانت الجهة الأخرى تتحقق من Unicode بصرامة.
3.3 \u2028 و\u2029
فواصل الأسطر وفواصل الفقرات صالحة في JSON، لكن تضمينها مباشرة في شيفرة JavaScript أو في وسم <script> يسبب خطأ نحويًا.
عند تضمين JSON داخل HTML، اهرّبها دائمًا أو انتقل إلى التحليل من نص عبر JSON.parse.
3.4 ثلاثة مصادر للنص الصيني المشوّه
- الملف نفسه بترميز GBK ويُقرأ بوصفه UTF-8؛
- ترويسة الاستجابة تعلن
charset=gbk، لكن المحتوى في الواقع UTF-8؛ - يمر عبر وسيط "يفك إلى نص ثم يعيد الترميز"، فيُعاد ترميز النص الصيني مرة ثانية.
ترتيب التحقيق: انظر إلى البايتات أولًا (xxd)، ثم ترويسة الاستجابة، وأخيرًا الوسيط.
4. null مقابل "هذا المفتاح غير موجود"
في JSON هذان الأمران مختلفان:
{"nickname": null}
{}
- الأول يعني "الحقل موجود بقيمة فارغة"؛
- الثاني يعني "الحقل غير موجود".
في TypeScript يصبح هذا هو الفرق بين string | null وnickname?: string؛ وفي Java يصبح الفرق بين "null صريح" و"قيمة افتراضية".
وبدون اصطلاح متفق عليه تظهر هذه الانقسامات:
// Frontend A's approach
if (data.nickname === undefined) { /* treat it as having no nickname */ }
// Frontend B's approach
if (!data.nickname) { /* handle null and the empty string together */ }
التوصية: يجب أن تنص وثائق API على "استخدم null بشكل موحد عند غياب القيمة، ولا تحذف المفتاح أبدًا"، أو العكس — لكن اختر واحدًا فقط؛
وعند تحديث API، استخدم دلالات PATCH لتجنب غموض "هل لم يُرسَل، أم أُرسل بقيمة null".
5. المصفوفات والكائنات
5.1 المصفوفات ليست "متجانسة"
[1, "two", {"three": 3}, null]
صالح نحويًا، لكن تعريف النوع يصبح شديد الصعوبة. في API، حاول إبقاء عناصر المصفوفة متجانسة؛ وفي الحالات غير المتجانسة انتقل إلى مصفوفة كائنات فيها حقل تمييز type.
5.2 مفاتيح الكائنات غير مرتبة، ومع ذلك يعتمد كثيرون على الترتيب
تقول المواصفة إن الكائن "مجموعة غير مرتبة من أزواج الاسم/القيمة". عمليًا يحافظ V8 على ترتيب الإدراج (باستثناء المفاتيح الصحيحة، التي تُرتَّب). بناء منطق على ترتيب المفاتيح (كأخذ "المفتاح الأول" مثلًا) يعني الاعتماد على تفصيل تنفيذي.
5.3 المصفوفات المتفرقة والفراغات غير موجودة
يتسلسل [1, , 3] في JavaScript إلى [1,null,3] — الفراغ يصبح null. ولا يوجد في JSON مفهوم "خانة".
6. قائمة تحقق قابلة للتطبيق
- [ ] كل المعرّفات والمبالغ النقدية والأعداد الصحيحة الضخمة نصوص صريحة في عقد API؛
- [ ] تنص وثائق API على دلالات
nullمقابل الحقل المحذوف؛ - [ ] حدود طول النصوص تحدد ما إذا كانت تُحسب بنقاط الشيفرة أم بوحدات UTF-16؛
- [ ] تُصفّى
Infinity/NaN/undefinedقبل التسلسل، أو تُحوَّل صراحةً إلىnull؛ - [ ] JSON المضمّن في HTML مهرَّب؛
- [ ] لدى الواجهة الأمامية والخلفية حالة اختبار عقدية تغطي "عدد صحيح كبير + إيموجي + null".
FAQ
كيف أتعامل مع الأعداد الصحيحة فوق 2^53 - 1؟
النهج الأقل إزعاجًا هو إرسال حقول مثل المعرّفات كنصوص على امتداد المسار كله، مثل {"id": "9007199254740993"}. وعلى الخادم يمكنك الحفاظ على الدقة بـ BigInteger / BigDecimal في Java، أو json.Number في Go، أو decimal.Decimal في Python. أما في الواجهة الأمامية، فيمكن لمحلل مثل json-bigint تحويل الحقل إلى BigInt، لكن تذكّر أن BigInt لا يمكن تمريره مباشرة إلى JSON.stringify.
لماذا يتحول Infinity وNaN إلى null بعد التسلسل؟
لا يقبل نوع number في JSON حرفيات غير معيارية مثل NaN وInfinity، لذا يحوّلهما JSON.stringify إلى null بصمت ودون أي خطأ. وهذه أصعب فئة من "البيانات اختفت فجأة" يمكن تتبعها. صفِّ هذه القيم صراحةً قبل التسلسل، أو حوّلها إلى null وفق اصطلاح عملك وسجّلها.
هل أرسل null أم أحذف الحقل تمامًا؟
كلاهما يعمل، لكن وثائق API يجب أن تختار واحدًا وتنص عليه بوضوح، وإلا افترضت كل واجهة أمامية شيئًا مختلفًا. في الأمثلة، يقابل {"nickname": null} و{} النوعين string | null وnickname?: string في TypeScript. استخدم دلالات PATCH عند تحديث API لتجنب غموض "هل لم يُرسَل، أم أُرسل بقيمة null".