نفس JSON، أربع أدوات، أربع نتائج: كيف تختار أداة التنسيق
الخلاصة: التنسيق ليس بلا فقدان. نفس ملف JSON الذي يحتوي صفرًا بادئًا أو BOM أو عددًا صحيحًا من 20 رقمًا يعطي نتائج مختلفة في Python وNode وjq وjson.tool: بعضها يطلق خطأ وبعضها يغيّر القيمة بصمت. عشر عينات نُفّذت محليًا عبر أربعة مسارات تنتج جدول اختيار وسكربت تحقق صارم.

الخلاصة: الفارق الأخطر بين أدوات تنسيق JSON ليس عدد مسافات الإزاحة، بل أن نفس الإدخال قد يُطلق خطأ في أداة ويُغيّر البيانات بصمت في أداة أخرى. قبل أن تختار، مرّر عيّنة واحدة تحمل قيمة حدّية عبر الأداة — فهي تخبرك أكثر من قائمة الميزات كلها.
يظنّ المستخدم أن التنسيق مجرد «إعادة ترتيب للنص». في الواقع كل مسار يعيد تفسير النص بمحلّله الخاص. وحين يكون المحلّل متسامحًا، يتحوّل الخطأ الذي كان يجب أن يُرمى إلى تعديل صامت لقيمة، بينما تظل الصفحة تعرض الرسالة الخضراء المطمئنة «تم التنسيق بنجاح».
1. الظاهرة: إدخال واحد، أربعة مسارات، أربع نتائج
بيئة الاختبار: Python 3.11.1 وNode v22.13.1 وjq 1.7. عشر سلاسل JSON قصيرة بُنيت يدويًا أُدخلت عبر الإدخال القياسي، دون كتابة على القرص ودون تغيير الإعدادات. المسارات الأربعة: json.loads وJSON.parse وjq -c . وpython -m json.tool. تعني ERR رمي استثناء ثم التوقف، وتعني SILENT عدم وجود خطأ لكن المخرجات اختلفت عن الإدخال.
| العيّنة | python json.loads | node JSON.parse | jq -c . | python -m json.tool |
|---|---|---|---|---|
S1 {"a":1,"b":[1,2]} |
OK | OK | OK | OK |
S2 {"a":1,} |
ERR | ERR | ERR | ERR |
S3 {'a':1} |
ERR | ERR | ERR | ERR |
S4 {"a":01} |
ERR char 6 | ERR position 6 | SILENT {"a":1} |
ERR |
S5 {"a":1 /*x*/} |
ERR char 7 | ERR | ERR | ERR |
S6 \ufeff{"a":1} |
ERR utf-8-sig | ERR position 0 | SILENT {"a":1} |
ERR |
S7 {"id":9007199254740993} |
OK exact | SILENT 9007199254740992 | OK exact | OK |
S8 {"a":1,"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
OK |
S9 {"a":1 |
ERR char 6 | ERR | ERR EOF | ERR |
S10 <html>502</html> |
ERR char 0 | ERR Unexpected token '<' | ERR | ERR |
أربع من العشر عيّنات أعطت «بلا خطأ لكن البيانات تغيّرت»: S4 وS6 وS7 وS8. أي أن من يثق بالضوء الأخضر «تم التنسيق بنجاح» وحده، لديه احتمال أربعين بالمئة بأن ما بين يديه لم يعد البيانات الأصلية. الماكر في الأمر أن أيا من هذه الحالات ليس في قائمة أخطاء الصياغة — الملف يُفتح ويُنسخ ويُصدَّر، والخطأ في المحتوى لا في الصياغة.
2. السبب: طبقة الصياغة وطبقة الدلالة شيئان مختلفان
يقول RFC 8259 بوضوح: يوصي القسم 1.2 بتجنّب ممارسات يسمح بها ECMA-404 من أجل أقصى توافقية؛ ويطلب القسم 4 أن تكون أسماء الكائنات فريدة كـSHOULD فقط، دون تحديد كيفية التعامل مع المفاتيح المكرّرة؛ ويعطي القسم 6 للأرقام مجرد توصيات توافقية. ويذهب ECMA-404 أبعد، إذ يعلن صراحة أنه «محايد تجاه دلالة الأرقام» — ينظّم تمثيل تسلسل الأرقام فقط، لا تخزينها داخل اللغة. بعبارة أخرى، تترك المواصفة للمُنفِّذين منطقة حدّية واسعة.
لذلك ينقسم «التنسيق» إلى طبقتين: طبقة الصياغة تقرّر هل النص قانوني، وطبقة الدلالة تقرّر إلى أي قيمة يتحوّل. للأولى إجابة قياسية، وللثانية لا. قراءة jq للقيمة 01 كـ1 وتجاوز BOM في البداية قرار في طبقة الدلالة؛ ودفع Node لعدد صحيح من 20 رقمًا إلى double IEEE 754 قرار وقت التشغيل؛ واحتفاظ المسارات الأربعة بالقيمة الأخيرة لـ{"a":1,"a":2} عرف مشترك بين المحلّلات الكبيرة. لا شيء يخالف «قابل للتحليل»، لكنها كلها تغيّر البيانات.
يعمل كيف تصلح أخطاء تحليل JSON على طبقة الصياغة، ويترجم نص الخطأ إلى إحداثيات. أما هذا المقال فيعالج الخطوة التي تسبقه مباشرة — إن لم تبلغ الأداة عن أي خطأ، فلن تحصل حتى على إحداثيات.
3. أربعة تغييرات صامتة للبيانات، واحدة بواحدة
إذا أفردنا العيّنات الصامتة الأربع، كانت التغييرات أصغر من أن تُلاحظ بلمحة عين:
{"a": 01} // leading zero: jq emits {"a":1}
\ufeff{"a": 1} // BOM: jq skips the first three bytes
{"id": 9007199254740993} // 20-digit int: JSON.parse gives ...992
{"a": 1, "a": 2} // duplicate key: all four keep {"a":2}
الصفر البادئ وBOM من نوع «الإدخال كان غير قانوني أصلًا، لكن الأداة تسامحت». 01 غير قانوني في JSON القياسي (RFC 8259 القسم 6)، ولا ينبغي أن يبدأ نص JSON بـBOM؛ كل من Python وNode يرفضان بوضوح، بينما يواصل jq. هذا التسامح مريح عند تنظيف السجلات، لكنه كارثي عند فحص العقد: يظنّ المنبع أنه أرسل بيانات خاطئة تحتاج تصحيحًا، بينما المصبّ يمسك بنتيجة «تبدو طبيعية».
العدد الصحيح الكبير من نوع «الإدخال قانوني تمامًا، لكن وقت التشغيل لا يتّسع له». 9007199254740993 رقم JSON قانوني، لكنه يتجاوز 2^53-1؛ ولا يستطيع JSON.parse إلا 9007199254740992. أمّا int في Python وjq فيحفظان القيمة الأصلية. إن كان في هذا JSON رقم طلب أو Snowflake ID، فإن تغيير آخر رقم يعني أن المطابقة المحاسبية لن تتطابق.
المفاتيح المكرّرة من نوع «المواصفة صامتة، والتنفيذ يختار». كل المسارات الأربعة تأخذ القيمة الأخيرة بصمت، دون أي تحذير. يقول RFC 8259 فقط SHOULD، لذا فهذا ليس خللًا — لكنه أصعب فئة يمكن إيجادها عند دمج الإعدادات أو تطبيق الرقع.
4. قدرة تحديد موقع الخطأ: من يعطي السطر والعمود، ومن يعطي جملة واحدة
حتى في العيّنات الست الفاشلة، يتفاوت قدر المعلومات بين المسارات الأربعة تفاوتًا كبيرًا. يعطي JSONDecodeError في Python كلًّا من msg وlineno وcolno وpos معًا؛ ويُبلغ Node v22 بـposition N (line 1 column M) مرفقًا بمقطع النص عند موضع الخطأ؛ ويستعيد python -m json.tool معلومات Python؛ أمّا jq فيعطي جملة واحدة parse error: ... at line N بلا إزاحة حرفية.
في JSON مُصغَّر بسطر واحد، يكاد رقم السطر والعمود يكون بلا قيمة — كل شيء في السطر 1. المطلوب هو إزاحة الحرف. يوحّد السكربت التالي الفحص في «خطأ + إزاحة + سياق»:
import json, sys
def strict_check(text: str) -> None:
try:
json.loads(text)
print("OK")
except json.JSONDecodeError as e:
print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
print("...", text[lo:hi].replace("\n", "\\n"), "...")
sys.exit(1)
strict_check(open(sys.argv[1], encoding="utf-8").read())
هو أشدّ من jq . (يرفض الصفر البادئ وBOM) ويعطي e.pos. أمّا كيف نحوّل line 1 column 23611 إلى موضع بايت، ولماذا تختلف الإزاحة الحرفية عن إزاحة البايت بثلاثة أضعاف في مستندات CJK، فمشروح في تصحيح JSON المضغوط ولا نكرّره هنا.
5. الإصلاح: قسّم التنسيق إلى «فحص صارم» + «تجميل»
الخلاصة بسيطة: لا تدع زر «تنسيق» واحدًا يحمل الفحص والتجميل معًا. استخدم محلّلًا صارمًا لتحديد الشرعية أولًا، ثم اختر أداة العرض. أمر واحد يفصل الخطوة الأولى:
python3 -m json.tool input.json > /dev/null && echo "syntax OK" || echo "invalid syntax"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json
السطر الأول يجيب فقط عن «هل يمكن تحليله؟»، والثاني يفحص «هل البنية الناتجة هي المتوقّعة؟». إن كانت البيانات تحوي أعدادًا صحيحة كبيرة، اقرأها بـPython أو jq لا بـJSON.parse؛ وإن كان المصدر قد يحمل BOM، حدّد صراحة encoding='utf-8-sig' عند القراءة بدل الاعتماد على تجاوز تلقائي من الأداة؛ وإن كانت المفاتيح قد تتكرّر، افحصها منفردة قبل التحليل، لأن أيًّا من المسارات الأربعة لا يحذّرك. مكتوبةً في CI، هذه هي بالضبط سلسلة الأنابيب التي يشرحها المحررات وسطر الأوامر وسلاسل أدوات الأتمتة، مع طبقة إضافية من تأكيدات أمانة البيانات.
6. كيف تختار: جدول مقارنة المسارات الأربعة
| المسار | نقطة القوة | المخاطرة | الأنسب لـ |
|---|---|---|---|
python json.loads / json.tool |
صارم، يعطي السطر/العمود والإزاحة الحرفية، لا يفقد دقة الأعداد الصحيحة | يرفض الصفر البادئ وBOM، ويحتاج معالجة صريحة للترميز | فحص العقود، CI، تتبّع السجلات |
node JSON.parse |
أصيل في المتصفح، الخطأ مرفق بمقطع من المصدر | الأعداد الصحيحة الكبيرة تُقتطع بصمت، BOM يسبّب خطأ | تصحيح الواجهة، أدوات داخل المتصفح |
jq -c . |
صديق للأنابيب، أسرع نتيجة | الصفر البادئ وBOM يمرّان بصمت، بلا إزاحة حرفية | أنابيب العمليات، استخراج حقل سريع |
أدوات المتصفح /parse و/editor |
عرض شجري، ترتيب المفاتيح، نسخ المسار، بلا تثبيت | السلوك يعتمد على محلّل الصفحة، لا بد من اختباره مرة | الفحص اليدوي، تتبّع أخطاء عابر |
مفتاح الاختيار ليس «أيّها أكثر ميزات»، بل «هل تكذب هذه الأداة على بياناتي؟». لبيانات تحوي أرقام طلبات أو مبالغ أو معرّفات أجهزة، فضّل Python أو أداة تعلن صراحة أمانة البيانات؛ ولمن يريد رؤية البنية فقط دون تخزين، المتصفح هو الأسهل. يعمل /parse في هذا الموقع على فحص الصياغة والتنسيق داخل المتصفح — مناسب لـ«ألقِ نظرة أولًا»، لكن عند الأعداد الصحيحة الكبيرة يُنصح بالتحقق مرة أخرى بالسكربت أعلاه.
7. الخصوصية ليست إلا أحد الحدود
«هل ستُرفع بياناتي؟» هو السؤال الأكثر تكرارًا مع الأدوات عبر الإنترنت، لكنه منفصل عن «هل ستُغيَّر بياناتي؟»: أداة محلية تعمل دون اتصال قد تقتطع عددًا صحيحًا من 20 رقمًا مع ذلك؛ وعكس ذلك، أداة عبر الإنترنت تبلغ أخطاءً صارمة قد تُرسل النص الأصلي إلى الخادم عبر POST. الخاصّتان تُفحصان منفصلتين ولا تُغني إحداهما عن الأخرى. يعطي سؤال الخصوصية ثلاثة فحوص بدقيقتين؛ ويكمل هذا المقال النصف الآخر — باستخدام قائمة العيّنات أدناه، تأكّد أن الأداة لا تغيّر القيم بصمت.
8. قائمة فحص ذاتي قابلة للنسخ
احفظ S1–S10 في cases.txt، عيّنة في كل سطر، وألصقها في أي أداة مرشّحة، وطابق ثلاثة أمور: هل فشلت عيّنات الخطأ فعلًا؟ هل ذكر الخطأ موضعًا محدّدًا؟ هل مخرجات العيّنة الناجحة مطابقة للإدخال حرفًا بحرف؟ افحص خصوصًا S4 وS6 وS7 وS8 — يكفي سطر SILENT واحد ليكون هذا المسار غير صالح وحده للتحقق من البيانات.
أضف ثلاثة قواعد هندسية: ضع الفحص الصارم في خطّاف pre-commit، ولا تكتشف تعارض الأعداد الصحيحة في الإنتاج؛ واكتب الملفات كـUTF-8 بلا BOM عبر اللغات كلها؛ واحتفظ بجسم الاستجابة الخام لـJSON من طرف ثالث، واستخدم النتيجة المنسّقة للقراءة فقط لا للكتابة العكسية.
الأسئلة الشائعة
نجح التنسيق، فهل يعني هذا أن JSON سليم؟
لا. نجحت S4 وS6 وS7 وS8 كلها في «التنسيق»، لكن الصفر البادئ وBOM والعدد الصحيح ذا 20 رقمًا والمفتاح المكرّر قد عُدِّلت أو ابتُلعت. يثبت التنسيق فقط أن محلّلًا ما قبل النص، لا أن القيم تساوي ما أرسلته؛ افحص الحقول الحرجة قيمةً بقيمة.
لماذا يقبل jq الصفر البادئ وBOM اللذين يرفضهما Python؟
لـjq محلّل معجمي خاص، متسامح مع أرقام مثل 01، ويتجاوز BOM في البداية؛ أمّا Python وNode فيلتزمان RFC 8259 بصرامة. ليس هذا خلل إصدار، بل الفرق بين محلّل متسامح وآخر صارم. عند التنظيف يوفّر الأول جهدًا، وعند فحص العقد الثاني إلزامي.
كيف أمنع عددًا صحيحًا كبيرًا من التحوّل إلى 9007199254740992؟
Number في JavaScript هو double IEEE 754 بحدٍّ آمن 2^53-1. قراءة معرّف من 20 رقمًا عبر JSON.parse تقتطعه حتمًا. خزّن هذه المعرّفات كسلاسل، أو استخدم int في Python أو jq أو مكتبة تحليل تدعم BigInt؛ وإن احتاجت الواجهة فعلًا رقمًا، استبدل الحقل بسلسلة قبل التحليل.
أداة عبر الإنترنت أم سطر أوامر محلي، أيّهما آخذ؟
قسّم بحسب الحساسية والغرض: لمجرّد رؤية البنية أو إيجاد حقل، المتصفح الأسرع؛ وللدخول في سلسلة أنابيب مع تأكيدات أمانة البيانات، استخدم python -m json.tool مع فاحص صارم. في الحالتين شغّل S1–S10 أولًا، وطابق الأخطاء والمخرجات بالمتوقَّع، ثم قرّر أيّ فئة بيانات تُدخلها.