وقتی دو سند JSON تفاوت دارند: diff موقعیتی، diff بر اساس id، یا اول نرمالسازی
جمعبندی: پنجاه رکورد به پنجاهویک میشود و diff پنجاهویک تغییر گزارش میکند. مثبت کاذب diff متنی، تطبیق آرایهها با id، نرمالسازی jq و patchهای قابل اعمال را پوشش میدهد.

جمعبندی: اول نرمالسازی، سپس مقایسه — از jq -S یا سریالایز پایدار برای پاک کردن ترتیب کلید، تورفتگی و املای عدد استفاده کنید، وقتی ترتیب بیمعناست آرایهها را بر اساس کلید کسبوکار مرتب یا ایندکس کنید، و تنها بعد diff را به patch تبدیل کنید.
تفاوت JSON تفاوت متن نیست. فایلها را بهصورت رشته مقایسه کنید و بازچینش کلید کل صفحه را روشن میکند؛ آرایهها را بر اساس اندیس مقایسه کنید و درج یک رکورد پنجاه رکورد دیگر را تغییریافته علامت میزند.
patchی به سرویس سفارشها منتشر شد و تنها چیزی که دارید دو پاسخ است: پنجاه رکورد پیش، پنجاهویک رکورد پس، با یک رکورد جدید درجشده در میانه. ابزار diff پنجاهویک تغییر گزارش میکند، دیواری از قرمز، و همچنان نمیفهمید کدام رکورد واقعاً جابهجا شد.
ابزار خراب نیست. «مقایسه» در JSON سه چیز متفاوت است و بیشتر مردم بهصورت پیشفرض سراغ بیفایدهترین میروند.
بازتولید هشدار کاذب: یک درج، پنجاهویک تغییر
سی خط Node 22: پنجاه سفارش بسازید ({id, sku, qty})، سپس یکی در انتها ضمیمه و یکی در ابتدا درج کنید و تفاوتها را با تطبیق اندیسبهاندیس بشمارید.
| عملیات | رکوردها | تغییرات گزارششده بر اساس موقعیت |
|---|---|---|
| ضمیمه کردن یکی در انتها | 50 → 51 | 1 |
| درج کردن یکی در ابتدا | 50 → 51 | 51 |
همان «یک رکورد اضافه»، پنجاهویک برابر نویز. دلیل پیشپاافتاده است: وقتی بر اساس اندیس مقایسه میکنید، عنصر جدید در موقعیت ۰ مینشیند و هر رکورد موجود یک جایگاه پایین میرود. موقعیت ۰ در برابر ۰، یک در برابر یک — هیچ جفتی برابر نیست.
بهجایش یک Map کلیدشده با id بسازید و همان داده دقیقاً یک ورودی گزارش میکند: $[id=ORD-999] added.
آن آزمایش قاعدهای میدهد که از هر توصیه ابزاری مهمتر است: اینکه آرایه دنبالهای مرتب است یا مجموعهای نامرتب تعیین میکند کدام مقایسه استفاده شود.
سه راه که diff متنی درباره JSON دروغ میگوید
diff a.json b.json نویسهها را مقایسه میکند. JSON ساختار داده حمل میکند. آن ناسازگاری سه نوع قابل اعتماد مثبت کاذب تولید میکند.
۱. کلیدهای بازچینششده شیء
بخش ۴ در RFC 8259 صریح است: شیء «مجموعهای نامرتب از جفتهای نام/مقدار» است. این دو سند مقدار یکساناند:
{ "id": 42, "name": "Ada", "active": true }
{ "active": true, "name": "Ada", "id": 42 }
یک diff خطی هر سه خط را تغییریافته میداند. ترتیب کلیدهای شیء دهکلیدی را وارونه کردم، هر دو را زیبا چاپ کردم و خطبهخط مقایسه کردم: همه ده خط علامت خوردند، در حالی که مقایسه ساختاری میگوید برابرند. نسبت نویز ۱۰:۰.
چرا اصلاً ترتیب کلید تغییر میکند؟ عوض کردن سریالایزر، اجرای قالببند با کلیدهای مرتب، بازسازی شیء از دیکشنری یا hash map، جابهجایی بین زمانهای اجرای زبان — هر یک کافی است تا داده یکسان روی دیسک متفاوت به نظر برسد.
۲. فضای سفید و تورفتگی
{"a":1} و {\n "a": 1\n} به مقدار یکسان تجزیه میشوند. diff متنی خطوط جدید و فاصلهها را میبیند و همهچیز را علامت میزند.
۳. املای متفاوت عددها و escapeها
1.0 و 1، 1e2 و 100، "\u00e9" و "é" — مقدار یکسان پس از تجزیه، متن متفاوت پس از سریالایز. این ملایمتر است چون خطوط کمی را لمس میکند، اما در بازبینی پیکربندی کافی است تا فکر کنید عددی تغییر کرده.
حالت معکوس همان چیزی است که متن نمیبیند و ساختار میبیند: 5 در برابر "5". یک جفت گیومه اضافه، اما نوع منحرف شده — معمولاً باگ است نه بهروزرسانی. مقایسه ساختاری بدون تردید آن را modify گزارش میکند.
سه حالت مقایسه، سه کار
| حالت | چه چیزی مقایسه میشود | نویز از کجا میآید | خروجی | مناسب |
|---|---|---|---|---|
| diff متنی | خطوط نویسه | ترتیب کلید، تورفتگی، املای عدد | شماره خط، قرمز/سبز | بستههای محلیسازی، fixtureهای با ترتیب کلید پایدار |
| diff ساختاری | درختهای تجزیهشده، همتراز بر اساس مسیر | آرایهها همتراز بر اساس اندیس | مسیرها با add/remove/modify | پاسخهای API، پیکربندیها، webhookها |
| diff معنایی | درختها بهعلاوه کلیدهای کسبوکار | فقط انتخاب بد کلید | مسیرها با تشخیص move/insert | فهرست رکوردها، سفارشها، موجودی |
diff ساختاری لایه «ترتیب کلید و قالببندی» را حل میکند. diff معنایی یک گام جلوتر میرود و «هویت عنصر آرایه» را حل میکند. تفاوت، پیچیدگی پیادهسازی نیست؛ این است که آیا حاضرید به ابزار بگویید کدام فیلد گذرنامه رکورد است.
آرایهها خط تقسیماند
یک پرسش بپرسید: آیا ترتیب این آرایه خودش داده است؟
- ترتیب داده است: مراحل مهاجرت، رتبهبندی جستوجو، نقاط نمودار، زنجیره تأیید. تغییر موقعیت تغییر رفتار است، پس مقایسه موقعیتی درست است.
- ترتیب بیمعناست: برچسبها، مجموعه شناسه مجوز، اقلام خط سفارش، فهرست موجودی. درج نباید بر قضاوت بقیه اقلام اثر بگذارد.
وقتی ترتیب بیمعناست، رکوردها را با یک کلید ایندکس کنید نه اینکه با موقعیت بکوبیدشان:
// Match by id: an insert or delete reports one entry, other items stay put
function diffById(before, after, path = '$') {
const out = [];
const beforeMap = new Map(before.map((item) => [item.id, item]));
const afterMap = new Map(after.map((item) => [item.id, item]));
for (const [id, value] of afterMap) {
if (!beforeMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'added' });
}
for (const [id, value] of beforeMap) {
if (!afterMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'removed' });
else if (JSON.stringify(value) !== JSON.stringify(afterMap.get(id))) {
out.push({ path: `${path}[id=${id}]`, type: 'changed' });
}
}
return out;
}
کتابخانههای بالغ این را به یک گزینه تبدیل میکنند. jsondiffpatch از LCS برای یافتن طولانیترین زیردنباله مشترک استفاده میکند، اما عناصر شیء داخل آرایهها بهصورت پیشفرض بر اساس موقعیت تطبیق داده میشوند — مستندات رسمی تأکید میکند باید تابع objectHash بدهید تا بر اساس محتوا تطبیق شوند. کاربران بار اول غالباً نتیجه میگیرند کتابخانه احمق است؛ در واقعیت آنها هرگز کلیدی ندادهاند.
انتخاب کلید مهم است. id انتخاب امن است. sku فقط وقتی کار میکند که SKUها یکتا باشند. هش کردن کل شیء «یک فیلد تغییر کرد» را به «یکی حذف شد بهعلاوه یکی اضافه شد» تبدیل میکند که از مقایسه موقعیتی بدتر است.
سریعترین ترفند ترمینال: نرمالسازی با jq
اگر کتابخانه نمیخواهید و فقط یک نگاه لازم دارید، اول نرمالسازی و دوم diff. jq -S کلیدهای شیء را بازگشتی مرتب و تورفتگی یکدست اعمال میکند:
# Normalize, then compare line by line; key order and indentation noise disappear
diff -u <(jq -S . before.json) <(jq -S . after.json)
# When the array is genuinely unordered, sort by key before comparing
diff -u <(jq -S '.items |= sort_by(.id)' before.json) \
<(jq -S '.items |= sort_by(.id)' after.json)
printf '{"b":2,"a":1,"arr":[1,2]}' | jq -S . را اجرا کردم و {"a":1,"arr":[1,2],"b":2} گرفتم — کلیدها مرتب، arr دستنخورده. یادتان باشد: -S فقط کلیدهای شیء را مرتب میکند، هرگز عناصر آرایه. یا آرایهها را صریح با sort / sort_by مرتب کنید، یا نویز موقعیتی را بپذیرید.
دو تله:
jqنمیتواند درجا ویرایش کند؛ به خروجی استاندارد مینویسد. از process substitution (<(...)) استفاده کنید، یا فایل موقت بنویسید، تأیید کنید، سپس جایگزین کنید؛-rگیومهها را از رشتهها حذف میکند، پس خروجی ممکن است خط جدید یا نویسههای معنادار برای شل داشته باشد. داده نامعتمد را پیش از لوله کردن گیومه بگذارید.
ابزار /diff این سایت چه میکند و چه نمیکند
ابزار مقایسه JSON داخلی همان رویکرد ساختاری را دنبال میکند: هر دو طرف به درخت تجزیه و بازگشتی بر اساس مسیر همتراز میشوند و path + type(add/remove/modify) + before/after تولید میکنند. ترتیب کلید شیء بر نتیجه اثر ندارد و همهچیز در مرورگر اجرا میشود — payloadهای جایگذاریشده هرگز آپلود نمیشوند.
اما آرایههایش بر اساس اندیس همتراز میشوند. این یک مصالحه عمدی است، نه نقص:
- ضمیمه در انتها، تغییر همان موقعیت یا جایگزینی یکجا خروجی تمیز میدهد؛
- درج رکورد در ابتدا یا میانه همه چیز بعدش را
modifyعلامت میزند و باید خودتان ببینید «واقعاً فقط یک رکورد جدید است».
پس وقتی فهرست سفارشها را مقایسه میکنید، حرکت درست مرتب کردن هر دو طرف بر اساس id پیش از جایگذاری است. ورودی مرتب نقطه درج را سنجاق میکند و نویز را به یک ورودی فرو میکوبد. بدون نصب، و بیشتر موارد روزمره را پوشش میدهد.
اگر به «تولید patch قابل اعمال» یا «تشخیص عناصر جابهجاشده» نیاز دارید، ابزار درونصفحه این کار را نمیکند. آن بخش بعد است.
تبدیل diff به patch: دو قالب، مخلوط نکنید
انسانها diff میخوانند؛ برنامهها patch اعمال میکنند. JSON دو قالب patch با معناشناسی کاملاً متفاوت دارد.
| بُعد | JSON Patch (RFC 6902) | JSON Merge Patch (RFC 7396) |
|---|---|---|
| شکل | آرایهای از عملیات با op، path، value |
شیء JSON به شکل سند هدف |
| آرایهها | بله، با اندیس، /items/0، /- برای ضمیمه |
نه، آرایهها یکجا جایگزین میشوند |
| حذف کلید | remove |
فقط با null، که نمیتواند فیلدی با مقدار قانونی null را حذف کند |
| بیان move | move / copy |
نه |
| مناسب | منابع حاوی آرایه، لاگ حسابرسی، بهروزرسانی اتمیک | بهروزرسانی جزئی اشیای ساده، تحویل پیکربندی |
JSON Patch یک عملیات دارد که مردم از آن رد میشوند: test.
[
{ "op": "test", "path": "/version", "value": 3 },
{ "op": "replace", "path": "/status", "value": "shipped" }
]
اگر test شکست بخورد، کل patch رد میشود. این همزمانی خوشبینانه تعبیهشده در پروتکل است — نیازی به سرآیند If-Match اضافه برای تضمین «فقط چون نسخه ۳ را دیدم نوشتم» نیست. برای endpoint PATCH این از هر قرارداد مستندات ارزشمندتر است.
مسیرها از JSON Pointer (RFC 6901) استفاده میکنند: ~0 برای ~، ~1 برای /. کلیدهای حاوی اسلش (مثلاً نوع MIME بهعنوان کلید) باید escape شوند، وگرنه pointer جای دیگری مینشیند. جزئیات در JSON Pointer و Patch است.
یک مشکل عملی میماند: patchهای تولیدشده خودکار از diff معمولاً آرایهها را با اندیس آدرسدهی میکنند. اندیسها در تولید شکنندهاند — یک درج همزمان و /items/3 دیگر به همان رکورد اشاره نمیکند. یا patchهای تولیدشده را به فیلدهای شیء محدود کنید، یا فیلدهای حیاتی را با test پیش از اعمال اعتبارسنجی کنید.
مقایسه را در CI بگذارید: مقادیر را اعلان کنید، نه متن
سه قاعده کافی است:
- در آزمونها مقادیر تجزیهشده را اعلان کنید، هرگز خروجی
JSON.stringifyرا. در Jest ازtoEqual؛ در Python پس ازjson.loadsازassert a == b. ترتیب کلید و تورفتگی شرکت نمیکنند. - فیلدهای پویا را در فهرست سفید بگذارید.
requestId،timestamp،traceIdهر اجرا متفاوتاند؛ بدون استثنا آزمون دائماً قرمز است.propertyFilterدر jsondiffpatch وexclude_pathsدر deepdiff برای همین وجود دارند. - هنگام شکست مسیرها را چاپ کنید، نه کل سند. هیچکس ۵ مگابایت payload در لاگ نمیخواند؛
$.data.items[id=ORD-042].qty: 2 → 3مشکل را فوراً مکانیابی میکند.
انحراف پیکربندی همینگونه کار میکند: پیکربندی staging و تولیدی را jq -S کنید، آرایهها را بر اساس کلید کسبوکار مرتب کنید و هر چه ماند تفاوت واقعی است. این دسته مشکل — همان داده که بین محیطها متفاوت به نظر میرسد — معمولاً قرارداد تعریفنشده است، که طراحی قرارداد API دربارهاش است.
پرسشهای متداول
آیا ترتیب کلید تفاوت حساب میشود؟
نه. RFC 8259 اشیا را مجموعههای نامرتب تعریف میکند، پس {"a":1,"b":2} و {"b":2,"a":1} مقدار یکساناند. هر ابزاری که بازچینش را تغییر گزارش کند، مقایسه متنی انجام میدهد نه مقایسه JSON.
چرا درج یک رکورد هر ورودی را تغییریافته علامت میزند؟
چون ابزار آرایهها را اندیسبهاندیس همتراز میکند. عنصر جدید در موقعیت ۰ مینشیند، هر رکورد موجود یک جایگاه پایین میرود و هیچ دو موقعیتی تطبیق ندارند. بهجای مقایسه موقعیتی، اول بر اساس کلیدی مانند id مرتب یا ایندکس کنید.
آیا ابزارهای مقایسه آنلاین دادههایم را آپلود میکنند؟
به پیادهسازی بستگی دارد. ابزارهای رندرشده در سرور payload را به بکاند میفرستند و ممکن است لاگ شود؛ ابزارهای سمت کلاینت در مرورگر تجزیه میکنند و چیزی آپلود نمیکنند. پیش از مقایسه پیکربندی تولیدی یا payloadهای کاربر، تأیید کنید صفحه محلی محاسبه میکند، یا شبکه را قطع کنید و دوباره امتحان کنید.
آیا نتیجه diff را میتوان مستقیم بهعنوان patch استفاده کرد؟
مستقیم نه. diff میگوید کجاها تفاوت دارد؛ JSON Patch میگوید کدام عملیات به ترتیب اجرا شوند. بینشان یک لایه تبدیل است. patchهای تولیدشده هم معمولاً آرایهها را با اندیس آدرسدهی میکنند که زیر همزمانی میشکند — پیش از اعمال عملیات test اضافه کنید.
چگونه فایلهای دهها مگابایتی را مقایسه کنم؟
اول جریان دهید (jq -c یک رکورد در هر خط مینویسد)، یا فقط زیردرخت مورد نظر را استخراج کنید، مانند jq '.data.items'. مقایسه بازگشتی کامل دهها مگابایت همچنان در حافظه جا میشود؛ بالاتر از صد مگابایت، تکهتکهاش کنید یا بر اساس کلید ایندکس کنید. برای اعداد مشخص حافظه و زمان، عملکرد JSON بزرگ را ببینید.
آیا باید آرایهها را پیش از مقایسه بر اساس id مرتب کنم؟
بله. ابزار مقایسه JSON داخلی آرایهها را بر اساس اندیس همتراز میکند، پس درج یک رکورد در ابتدا یا میانه همه چیز بعدش را modify علامت میزند؛ مرتب کردن هر دو طرف بر اساس id پیش از جایگذاری نقطه درج را سنجاق میکند و نویز را به یک ورودی فرو میکوبد. استثنا وقتی است که ترتیب آرایه خودش داده است — مراحل مهاجرت، رتبهبندی جستوجو، نقاط نمودار — که مقایسه موقعیتی درست است و مرتب کردن اشتباه.