JSON Toolkit

الأنواع الستة ودقة الأعداد ومزالق الترميز

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

مبتدئقراءة نحو 12 دقيقةنُشر في 2026-10-30حُدِّث في 2026-10-30
غلاف مقال الأنواع الستة ودقة الأعداد ومزالق الترميز

الخلاصة: تحقق أولًا مما إذا كانت حقول المعرّفات أو المبالغ النقدية تتجاوز 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) سيواجهها.

العلاجات (مرتبة حسب مقدار توصيتي بها):

  1. استخدم النصوص للمعرّفات على امتداد المسار كله: {"id": "9007199254740993"}، وهو الأقل إزعاجًا على الإطلاق؛
  2. حلّل بأنواع عالية الدقة على الخادم: BigInteger / BigDecimal في Java، وjson.Number في Go، وdecimal.Decimal في Python؛
  3. في الواجهة الأمامية، استخدم دالة 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 ثلاثة مصادر للنص الصيني المشوّه

  1. الملف نفسه بترميز GBK ويُقرأ بوصفه UTF-8؛
  2. ترويسة الاستجابة تعلن charset=gbk، لكن المحتوى في الواقع UTF-8؛
  3. يمر عبر وسيط "يفك إلى نص ثم يعيد الترميز"، فيُعاد ترميز النص الصيني مرة ثانية.

ترتيب التحقيق: انظر إلى البايتات أولًا (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".