الأسئلة الشائعة حول الصياغة والتنسيق: التعليقات والفواصل الزائدة والدقة
الخلاصة: هل التعليقات مسموحة، ولماذا تسبب الفاصلة الزائدة خطأ، وهل تحتاج المفاتيح إلى اقتباسات، ولماذا يتغير 9007199254740993 — اثنا عشر استنتاجًا، كل واحد منها مفحوص وفق RFC 8259.

الخلاصة: اكتب JSON خالصًا وفق RFC 8259 — اقتباسات مزدوجة، بلا تعليقات، بلا فواصل زائدة، UTF-8 دون BOM — لكل ما يعبر حدود عملية؛ واستخدم JSONC أو JSON5 للتهيئة التي تحتاج إلى تعليقات وحوّل عند التشغيل؛ وسلسل الأعداد الصحيحة فوق 2^53-1 كنصوص على الخادم.
ما يجمع هذه الأسئلة: تبدو كأنها "يجب أن تعمل"، لكن المواصفة صارمة جدًا في الواقع.
1. ما يُعد JSON صالحًا
ما الفرق بين JSON وJSON5 وJSONC؟
JSON القياسي لا يعترف إلا بالسلاسل النصية ذات الاقتباسات المزدوجة، ولا يسمح بتعليقات ولا بفواصل زائدة. أما JSON5 وJSONC فيضيفان ميزات فوقه:
| الميزة | JSON (RFC 8259) | JSONC | JSON5 |
|---|---|---|---|
| السلاسل النصية باقتباسات مزدوجة | مدعومة | مدعومة | مدعومة |
| السلاسل النصية باقتباسات مفردة | غير مدعومة | غير مدعومة | مدعومة |
| التعليقات | غير مدعومة | مدعومة | مدعومة |
| الفواصل الزائدة | غير مدعومة | غير مدعومة | مدعومة |
| المفاتيح بلا اقتباسات | غير مدعومة | غير مدعومة | مدعومة |
| الأعداد الست عشرية | غير مدعومة | غير مدعومة | مدعومة |
NaN / Infinity |
غير مدعومة | غير مدعومة | مدعومة |
| امتداد الملف | .json |
.jsonc |
.json5 |
مبدأ الاختيار بسيط: استخدم دائمًا JSON القياسي عند عبور حدود العمليات، بينما قد تستخدم ملفات التهيئة JSONC أو JSON5 حسب ما يدعمه محررك.
يمكن أن يحتوي tsconfig.json و.vscode/settings.json على تعليقات لأن المحرر يحللهما كـ JSONC؛ لكن تمريرهما إلى
JSON.parse صارم ما زال يخطئ.
هل يمكن أن يحتوي JSON على تعليقات؟
لا. القواعد في RFC 8259 ليس فيها إنتاج للتعليق، فأي // أو /* */ يجعل التحليل يفشل.
إذا أردت الاحتفاظ بالتوضيحات في التهيئة، فهناك ثلاثة بدائل:
{
"_comment": "以下超时单位是毫秒",
"timeout": 3000,
"retry": {
"$doc": "重试次数,0 表示不重试",
"count": 3
}
}
- استخدم حقولًا اصطلاحية مثل
_commentيتجاهلها القارئ ببساطة؛ - استخدم حقولًا تبدأ بـ
$لتقليل احتمال تعارضها مع حقول العمل؛ - انتقل ببساطة إلى JSONC أو YAML للتهيئة وحوّل إلى JSON قياسي عند التشغيل.
لماذا تسبب الفاصلة الزائدة خطأ؟
لأن المصفوفات والكائنات في القواعد كلاهما بنية "عنصر + فاصل"، ولا يُسمح بفاصلة بعد العنصر الأخير.
{
"a": 1,
"b": 2,
}
المقطع أعلاه فيه فاصلة إضافية قبل }، وغالبًا يُبلَّغ عن الخطأ عند الفاصلة أو عند القوس الختامي حسب تطبيق المحلل.
وهذا أيضًا أسهل فخ تصطدم به عند الانتقال من JavaScript — كائنات JS الحرفية تسمح بالفواصل الزائدة، وJSON لا.
هل الاقتباسات المفردة مسموحة؟
لا. يجب أن تستخدم السلاسل النصية اقتباسات مزدوجة؛ 'text' خطأ في الصياغة.
إذا كانت البيانات منسوخة من repr() في Python، أو من سكربت shell، أو من تهيئة مكتوبة يدويًا، فحوّلها بأداة أولًا
بدلًا من الأمل في أن يكون المحلل متسامحًا.
هل يمكن أن تكون المفاتيح بلا اقتباسات؟
لا. يجب أن تكون أسماء أعضاء الكائن سلاسل نصية محاطة باقتباسات مزدوجة.
{ name: "json" }
هذا كائن JavaScript حرفي، وليس JSON. إضافة الاقتباسات تجعله صالحًا:
{ "name": "json" }
هل يمكن أن يكون المستوى الأعلى مصفوفة أو سلسلة نصية؟
نعم. يسمح RFC 8259 بأي قيمة JSON في المستوى الأعلى: الكائنات والمصفوفات والسلاسل النصية والأعداد وtrue وfalse وnull كلها صالحة.
لكن في الممارسة الهندسية يُستحسن استخدام الكائنات أو المصفوفات فقط في المستوى الأعلى، لسببين:
- التوافق: المحللات القديمة (من جيل RFC 4627) تشترط أن يكون المستوى الأعلى كائنًا أو مصفوفة؛
- الأمان: حملت المصفوفة في المستوى الأعلى تاريخيًا خطر اختطاف JSON. أصلحت المتصفحات الحديثة ذلك، لكن الأنظمة القديمة ما زالت تفترض الاصطلاح.
لذا "hello" JSON صالح و["a", "b"] كذلك، لكن API يعيد {"data": [...]} أكثر أمانًا.
2. الترميز والسلاسل النصية
هل يعطّل BOM في بداية الملف التحليل؟
نعم. يشترط RFC 8259 صراحةً نقل نص JSON بترميز UTF-8 وألّا يضيف علامة ترتيب البايتات (BOM).
BOM هو البايتات الثلاثة EF BB BF في بداية الملف تمامًا. إنه غير مرئي بالكامل، لكنه يشغل الموضع الأول من السلسلة،
فيستنتج المحلل أن "الحرف الأول ليس {" ويخطئ — وغالبًا ما يكون السطر والعمود المبلَّغان السطر 1 والعمود 1، وهو ما يبدو محيّرًا.
خيار "Save as UTF-8" في Notepad على Windows يتضمن BOM افتراضيًا. للتحقق منه وإزالته:
# Check: output starting with efbbbf means a BOM is present
head -c 3 data.json | xxd
# Remove: skip the first 3 bytes with tail
tail -c +4 data.json > clean.json
JSON.parse في JavaScript الحديث يتحمل BOM (يتخطاه V8 داخليًا)، لكن التطبيقات الصارمة في Java وGo وPython لا تفعل،
وهذا سبب شائع لأن "الملف نفسه يعطي نتائج مختلفة في لغات مختلفة".
هل تحتاج الأحرف الصينية إلى تحويل إلى \uXXXX؟
لا. يشترط RFC 8259 أن يكون نص JSON بترميز UTF-8، فيمكن أن يظهر النص الصيني مباشرة؛ {"name": "张三"} صالح تمامًا.
\uXXXX مجرد تهجئة بديلة مكافئة، تُستخدم للقنوات التي لا تستطيع تمثيل الحرف مباشرة. القاعدة التقريبية:
- أجسام طلبات HTTP والملفات وقواعد البيانات: اكتب النص الصيني مباشرة، موفرًا 3 بايتات لكل حرف؛
- ترويسات HTTP ومعلمات URL والتضمين داخل سلسلة أخرى: استخدم الهروب لتجنب مشكلات الترميز.
الأحرف الوحيدة التي يجب تهريبها هي أحرف التحكم (U+0000 حتى U+001F). مثلًا يجب كتابة السطر الجديد على هيئة \n؛
لا يمكنك وضع سطر جديد حرفي داخل سلسلة نصية.
هل يمكن أن تدخل الرموز التعبيرية مباشرة في سلسلة نصية؟
نعم، لكن تذكّر أنها تشغل وحدتي رمز UTF-16.
الرموز التعبيرية وكثير من الأحرف النادرة (مثل 𠮷) خارج BMP، لذا في JavaScript "😀".length === 2.
لذا يسهل الخطأ عند التحقق بعدد الأحرف:
const text = "😀";
console.log(text.length); // 2, number of UTF-16 code units
console.log([...text].length); // 1, number of code points
console.log(Buffer.byteLength(text)); // 4, number of UTF-8 bytes
إذا حُسب حد طول كنية بـ.length، فرمز تعبيري واحد يستهلك موضعين؛ وإن عدّ الخادم البايتات فهو أربعة.
عند توحيد التعريفات، صرّح بوضوح هل تقصد نقاط الترميز أم وحدات الرمز أم البايتات.
3. الأعداد والقيم الخاصة
لماذا تحوّل العدد الصحيح الكبير 9007199254740993 إلى 9007199254740992؟
لأن JSON.parse في JavaScript يحول الأعداد إلى أعداد عائمة مزدوجة الدقة IEEE 754، وأكبر عدد صحيح آمن هو 2^53-1.
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
9007199254740993 // actually yields 9007199254740992
أي عدد فوق 2^53-1 قد يُقرَّب في أرقامه الأخيرة. الضحايا المعتادون هم معرّفات المستخدمين ذات 19 رقمًا المولَّدة بخوارزميات snowflake، وأرقام الطلبات، والطوابع الزمنية بالنانوثانية.
ثلاثة إصلاحات مرتبة حسب التفضيل:
- سلسل المعرّف كنص على الخادم:
"id": "9007199254740993"، ما يزيل مشكلة الدقة من جذرها؛ - استخدم مكتبة تحليل بلا فقد: بدائل
JSON.parseيمكنها حفظ الأعداد الصحيحة الكبيرة كما هي، مثلjson-bigint؛ - استخرج بتعبير نمطي للعرض فقط: لا تحلل الوثيقة كلها، طابق الحقل الهدف كنص فقط.
لاحظ أن JSON.stringify له مشكلة متماثلة: عدد خارج نطاق الأعداد الصحيحة الآمنة قد لا يحافظ على قيمته الأصلية بعد التسلسل.
هل يمكن أن يدخل NaN وInfinity في JSON؟
لا. إنتاج الأعداد في JSON يسمح بالتدوين العشري فقط، وNaN وInfinity و-Infinity ليست قيم JSON صالحة.
مقاربة JavaScript هي "تدهور صامت":
JSON.stringify({ value: NaN }); // '{"value":null}'
JSON.stringify({ value: Infinity }); // '{"value":null}'
لا خطأ، NaN يصبح null ببساطة. هذا المشهد الكلاسيكي حيث "أرسلت الواجهة NaN واستقبل الخادم null".
عندما تحتاج إلى نقل هذه الدلالة، عبّر عنها صراحةً بسلاسل نصية أو قيم حارسة متفق عليها:
{ "ratio": null, "ratioState": "not-a-number" }
هل يمكن كتابة الأعداد على هيئة 1e3 أو 0.5؟
نعم. أعداد JSON تدعم الكسور والأسس، لذا 1e3 و1E3 و0.5 و-0.5 كلها صالحة.
لكن هناك قيود:
- لا يمكن أن تبدأ أو تنتهي بـ
.:.5و5.غير صالحتين، فاكتب0.5و5؛ - لا أصفار بادئة:
01غير صالح؛ - لا نظام ست عشري:
0x1Fغير صالح، فهذا امتداد JSON5؛ -0صالح، لكنJSON.stringify(-0)يخرج0، فيفقد الإشارة.
4. دلالات الكائن
ماذا يحدث مع المفاتيح المكررة؟
السلوك يحدده التطبيق؛ يقول RFC 8259 فقط إن أسماء المفاتيح "ينبغي" أن تكون فريدة ولا يحدد ما يحدث عند التكرار.
عمليًا تأخذ الغالبية العظمى من التطبيقات القيمة الأخيرة:
JSON.parse('{"a":1,"a":2}') // { a: 2 }
لكن لا يمكنك الاعتماد على ذلك. بعض مكتبات Java، وencoding/json في Go، وjson في Python تختلف بين الإصدارات:
بعضها يأخذ القيمة الأولى، وبعضها يخطئ، وبعضها يأخذ الأخيرة. معاملة المفاتيح المكررة كخطأ هي المقاربة الأكثر أمانًا عبر اللغات.
لا يمكنك العثور على المفاتيح المكررة بالعين — في JSON المصغّر قد يفصل المفتاح نفسه آلاف الأحرف. استخدم سكربتًا:
# Extract keys that appear more than once at the same level, as a rough check
jq -r 'paths(scalars) | join(".")' data.json | sort | uniq -d
إلى ماذا يتحول undefined؟
يعتمد على الحال، وهذه أكثر نقطة عرضة للأخطاء:
JSON.stringify({ a: undefined }); // '{}' the key is dropped entirely
JSON.stringify({ a: undefined, b: 1 }); // '{"b":1}'
JSON.stringify([undefined]); // '[null]' becomes null inside arrays
JSON.stringify({ a: function () {} }); // '{}' functions are dropped too
undefined نفسه يعني "يختفي الحقل" داخل كائن و"يصبح null" داخل مصفوفة.
إذا كان الخادم يسلك مسارات مختلفة لـ"الحقل غائب" و"الحقل null"، فستحصل على تعارض هنا.
عندما تحتاج إلى التعبير عن "لا قيمة" صراحةً، استخدم null بدلًا من undefined.
ماذا يحدث بعد تحليل {"__proto__": {...}}؟
JSON.parse نفسه آمن: يُعامل __proto__ كخاصية ذاتية عادية ولا يغيّر prototype.
الخطير هو عملية الدمج بعد التحليل:
const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch); // some implementations pollute the prototype
المقاربة الصحيحة هي التكرار على الخصائص الذاتية وتخطي __proto__ وconstructor وprototype.
التفاصيل في الأسئلة الشائعة حول الأداء والأمان.
5. مرجع سريع
| الصياغة | صالحة | ملاحظات |
|---|---|---|
{"a": 1,} |
لا | فاصلة زائدة |
{'a': 1} |
لا | اقتباسات مفردة |
{a: 1} |
لا | مفتاح بلا اقتباسات |
{"a": 1} // comment |
لا | تعليقات |
"hello" |
نعم | سلسلة نصية في المستوى الأعلى صالحة، لكنها نادرة الاستخدام في APIs |
{"a": 1e3} |
نعم | الأسس مدعومة |
{"a": 01} |
لا | صفر بادئ |
{"a": NaN} |
لا | حرفي غير صالح؛ stringify يحوّله إلى null |
\uFEFF{"a":1} |
لا | BOM ليس جزءًا من نص JSON |
{"a":1,"a":2} |
متنازع عليه | التطبيقات تأخذ القيمة الأخيرة، لكن لا تعتمد على ذلك |