كثيرًا ما تحتاج إلى مقارنة بيانات بصيغة JSON، مثل استجابات API وملفات الإعدادات وملفات الترجمة. لكن إذا قارنت ملفي JSON كنص كما هما، فكثيرًا ما تظهر فروق كثيرة مع أن البيانات واحدة. يشرح هذا المقال السبب وطريقة الحل.
لماذا تظهر فروق زائفة؟
يعرّف معيار JSON (RFC 8259) الكائن بأنه «مجموعة غير مرتبة من أزواج الاسم/القيمة». أي أن {"a":1,"b":2} و{"b":2,"a":1} يمثلان البيانات نفسها. لكن ترتيب الأسطر في النص مختلف، فيعدّه diff فرقًا. وإضافة إلى ذلك تنشأ الفروق التالية دون علاقة بالبيانات:
- عرض المسافة البادئة (مسافتان، أربع مسافات، علامة جدولة) وJSON مضغوط في سطر واحد
- وجود مسافة بعد النقطتين أو عدمه (
"a":1و"a": 1) - طريقة كتابة الأرقام (
1.0و1، و1e2و100) - تهريب الأحرف في السلاسل النصية (
"\u00e9"و"é")
الحل: التحليل ثم إعادة الكتابة بالشكل نفسه
الطريقة الأضمن هي تحليل ملفي JSON إلى بيانات ثم إعادة تسلسلهما بالقواعد نفسها.
- حلّل النصين كـ JSON. إن وُجد خطأ في الصياغة فأصلحه أولًا.
- رتّب مفاتيح جميع الكائنات تكراريًا.
- أعد الكتابة بالمسافة البادئة نفسها (مثل مسافتين).
- قارن النتيجة سطرًا بسطر.
على سبيل المثال، يختلف ملفا JSON التاليان في ترتيب المفاتيح وفي ترتيب عناصر المصفوفة.
{"name":"kim","age":30,"tags":["a","b"]}
{"age":30,"name":"kim","tags":["b","a"]}
بعد الترتيب وإعادة الكتابة يختفي فرق ترتيب المفاتيح، ولا يبقى إلا الفرق الحقيقي وهو ترتيب المصفوفة.
@@ -2,7 +2,7 @@ "age": 30, "name": "kim", "tags": [- "a",- "b"+ "b",+ "a" ] }
لا تُرتَّب المصفوفات
على عكس الكائنات، للترتيب في المصفوفات معنى. ["a","b"] و["b","a"] بيانات مختلفة. لذلك فالأصل ألا تُرتَّب المصفوفات عند التوحيد. لكن إذا كانت المصفوفة مثل قائمة الوسوم لا يهم ترتيبها في العمل، فإن ترتيبها يدويًا قبل المقارنة يجعل النتيجة أسهل قراءة. أيّ الأمرين هو الصحيح يتوقف على معنى البيانات.
ما الذي يغيّره التحليل؟
لطريقة التحليل ثم إعادة الكتابة آثار جانبية يجب معرفتها.
- توحيد كتابة الأرقام. يصبح
1.0و1كلاهما1فيبدوان متطابقين. هذا مرغوب غالبًا، لكن إذا كانت طريقة الكتابة نفسها مهمة فقارن النص الأصلي أيضًا. - دقة الأعداد الصحيحة الكبيرة. تتعامل جافاسكربت مع الأرقام كأعداد عشرية عائمة بطول 64 بت، فلا تستطيع تمثيل الأعداد الصحيحة الأكبر من
Number.MAX_SAFE_INTEGER(2^53 − 1 = 9007199254740991) بدقة. مثلًا يصبح9007199254740993بعد التحليل9007199254740992. انتبه لذلك في ملفات JSON التي تُخزَّن فيها معرّفات طويلة كأرقام. - المفاتيح المكررة. يكتفي RFC 8259 بالتوصية (SHOULD) بأن تكون الأسماء داخل الكائن فريدة، وطريقة التعامل مع المفاتيح المكررة تختلف من تطبيق لآخر. تحتفظ
JSON.parseفي جافاسكربت بالقيمة الأخيرة. - التعليقات والفواصل الختامية.
// commentوالفواصل الختامية مثل[1, 2,]ليست جزءًا من JSON القياسي (تسمح بها صيغ موسّعة مثل JSON5 وJSONC). والمحلّل القياسي يُظهر خطأ.
الطريقة من سطر الأوامر
إذا كانت أداة jq مثبتة فيمكنك باستخدام الخيار -S (--sort-keys) إخراج JSON بمفاتيح مرتبة.
jq -S . before.json > a.json
jq -S . after.json > b.json
diff -u a.json b.json
الفرق عن JSON Patch
نتيجة diff قائمة تغييرات على مستوى السطر موجّهة للقراءة البشرية. أما JSON Patch (RFC 6902) فمعيار يعبّر عن التغييرات بمسارات داخل بنية JSON، مثل {"op":"replace","path":"/age","value":31}، وJSON Merge Patch (RFC 7396) طريقة تُطبّق على الأصل JSON يحتوي على الأجزاء المتغيرة فقط. إذا كانت البرامج تتبادل التغييرات فيما بينها فهذه المعايير هي الأنسب، وإذا كان الإنسان هو من يراجع فالأنسب diff نصي بعد الترتيب.
جرّبها في هذه الأداة
إذا فعّلت خيار ترتيب مفاتيح JSON في مقارنة النصوص، يُحلَّل الجانبان وتُرتَّب المفاتيح ويُعاد الكتابة بمسافة بادئة من مسافتين ثم تجري المقارنة. لا يتغير ترتيب المصفوفات، وإذا فشل التحليل تخبرك الأداة في أي جهة وعند أي حرف وقع الخطأ. يمكنك أيضًا سحب ملفي .json وإفلاتهما معًا. ولقراءة النتيجة كـ patch راجع مقال كيف تقرأ unified diff.