দুটি JSON ডকুমেন্ট যখন আলাদা: অবস্থানভিত্তিক ডিফ, id-ভিত্তিক ডিফ, নাকি আগে নরমালাইজ
সংক্ষেপে: পঞ্চাশ রেকর্ড একান্ন হয়, আর ডিফ একান্নটি পরিবর্তন রিপোর্ট করে। টেক্সট-ডিফের ভুয়া সংকেত, id দিয়ে অ্যারে মেলানো, jq নরমালাইজেশন ও প্রযোজ্য প্যাচ।

মূল কথা: আগে নরমালাইজ, তারপর তুলনা — jq -S বা স্থিতিশীল সিরিয়ালাইজেশন দিয়ে কি-ক্রম, ইনডেন্টেশন ও সংখ্যার বানান মুছুন, ক্রম অর্থহীন হলে অ্যারে ব্যবসায়িক কি দিয়ে সাজান বা ইনডেক্স করুন, আর কেবল তারপর ডিফকে প্যাচে বদলান।
JSON পার্থক্য টেক্সট পার্থক্য নয়। ফাইলগুলো স্ট্রিং হিসেবে তুলনা করলে একটি কি পুনর্বিন্যাস পুরো পৃষ্ঠা জ্বালিয়ে দেয়; অ্যারে ইনডেক্স দিয়ে তুলনা করলে একটি রেকর্ড ঢোকালে বাকি পঞ্চাশটিকে বদলানো দেখায়।
অর্ডার সার্ভিসে একটি প্যাচ গেছে, আর আপনার হাতে কেবল দুটি রেসপন্স: আগে পঞ্চাশ রেকর্ড, পরে একান্ন, মাঝখানে একটি নতুন রেকর্ড ঢোকানো। ডিফ টুল একান্নটি পরিবর্তন রিপোর্ট করে, লালের দেয়াল, আর আপনি তবু বলতে পারেন না কোন রেকর্ড আসলে সরেছে।
টুল নষ্ট নয়। JSON-এ "তুলনা" তিন ভিন্ন অর্থ বোঝায়, আর বেশিরভাগ মানুষ ডিফল্টে সবচেয়ে কম উপকারীটির দিকে হাত বাড়ায়।
ভুয়া সংকেত পুনরুৎপাদন: একটি ইনসার্ট, একান্নটি পরিবর্তন
Node 22-এ ত্রিশ লাইন: পঞ্চাশটি অর্ডার ({id, sku, qty}) বানিয়ে শেষে একটি যোগ করুন ও শুরুতে একটি ঢোকান, ইনডেক্স-ধরে-ইনডেক্স মিলিয়ে পার্থক্য গুনুন।
| অপারেশন | রেকর্ড | অবস্থান দিয়ে রিপোর্ট হওয়া পরিবর্তন |
|---|---|---|
| শেষে একটি যোগ | ৫০ → ৫১ | ১ |
| শুরুতে একটি ঢোকানো | ৫০ → ৫১ | ৫১ |
একই "একটি বাড়তি রেকর্ড", একান্ন গুণ শব্দ। কারণ সাধারণ: ইনডেক্স দিয়ে তুলনা করলে নতুন এলিমেন্ট অবস্থান ০-এ পড়ে আর প্রতিটি বিদ্যমান রেকর্ড এক স্লট নিচে সরে। অবস্থান ০ বনাম অবস্থান ০, অবস্থান ১ বনাম অবস্থান ১ — কোনো জোড়া সমান নয়।
বদলে id দিয়ে কি করা একটি Map বানালে একই ডেটা ঠিক একটি এন্ট্রি রিপোর্ট করে: $[id=ORD-999] added।
এই পরীক্ষা যেকোনো টুল সুপারিশের চেয়ে বেশি গুরুত্বপূর্ণ নিয়ম দেয়: অ্যারেটি ক্রমবদ্ধ ক্রম নাকি ক্রমহীন সেট — সেটাই ঠিক করে কোন তুলনা ব্যবহার করবেন।
টেক্সট ডিফ JSON নিয়ে যে তিনভাবে মিথ্যা বলে
diff a.json b.json অক্ষর তুলনা করে। JSON ডেটা কাঠামো বহন করে। সেই অমিল তিন ধরনের নির্ভরযোগ্য ভুয়া সংকেত আনে।
১. পুনর্বিন্যস্ত অবজেক্ট কি
RFC 8259 ধারা ৪ স্পষ্ট: অবজেক্ট "নাম/মান জোড়ার ক্রমহীন সংগ্রহ"। এই দুই ডকুমেন্ট একই মান:
{ "id": 42, "name": "Ada", "active": true }
{ "active": true, "name": "Ada", "id": 42 }
লাইন ডিফ তিনটি লাইনই বদলানো বলে। আমি দশ-কি অবজেক্টের কি-ক্রম উল্টে দুইটিই প্রিটি-প্রিন্ট করে লাইন ধরে তুলনা করেছি: দশটি লাইনই চিহ্নিত, অথচ কাঠামোগত তুলনা বলে তারা সমান। শব্দের অনুপাত ১০:০।
কি-ক্রম আদৌ বদলায় কেন? সিরিয়ালাইজার বদলানো, কি-সাজানো ফরম্যাটার চালানো, অভিধান বা হ্যাশ ম্যাপ থেকে অবজেক্ট পুনর্গঠন, ভাষার রানটাইম বদলানো — যেকোনো একটি একই ডেটাকে ডিস্কে আলাদা দেখাতে যথেষ্ট।
২. হোয়াইটস্পেস ও ইনডেন্টেশন
{"a":1} ও {\n "a": 1\n} একই মানে পার্স হয়। টেক্সট ডিফ নিউলাইন ও স্পেস দেখে সব চিহ্নিত করে।
৩. সংখ্যা ও এস্কেপের ভিন্ন বানান
1.0 ও 1, 1e2 ও 100, "\u00e9" ও "é" — পার্সের পর একই মান, সিরিয়ালাইজের পর আলাদা টেক্সট। এটি মৃদু, কারণ কম লাইন ছোঁয়, কিন্তু কনফিগ রিভিউতে একটি সংখ্যা বদলেছে ভাবতে যথেষ্ট।
উল্টো ক্ষেত্রটি টেক্সট দেখতে পায় না, কাঠামো পারে: 5 বনাম "5"। এক জোড়া বাড়তি কোট, কিন্তু টাইপ সরে গেছে — সাধারণত হালনাগাদ নয়, বাগ। কাঠামোগত তুলনা দ্বিধা না করে modify রিপোর্ট করে।
তিন তুলনা মোড, তিন কাজ
| মোড | কী তুলনা হয় | শব্দ কোথা থেকে | আউটপুট | উপযুক্ত |
|---|---|---|---|---|
| টেক্সট ডিফ | অক্ষরের লাইন | কি-ক্রম, ইনডেন্টেশন, সংখ্যার বানান | লাইন নম্বর, লাল/সবুজ | লোকালাইজেশন বান্ডল, স্থিতিশীল কি-ক্রমযুক্ত ফিক্সচার |
| কাঠামোগত ডিফ | পার্স করা ট্রি, পাথ দিয়ে মেলা | অ্যারে ইনডেক্স দিয়ে মেলা | add/remove/modify সহ পাথ | API রেসপন্স, কনফিগ, webhook |
| সেমান্টিক ডিফ | ট্রি প্লাস ব্যবসায়িক কি | কেবল খারাপ কি-নির্বাচন | move/insert শনাক্তকরণসহ পাথ | রেকর্ড তালিকা, অর্ডার, ইনভেন্টরি |
কাঠামোগত ডিফ "কি-ক্রম ও ফরম্যাটিং" স্তর সমাধান করে। সেমান্টিক ডিফ এক ধাপ এগিয়ে "অ্যারে এলিমেন্টের পরিচয়" সমাধান করে। পার্থক্য বাস্তবায়ন জটিলতায় নয়; আপনি টুলকে বলতে রাজি কি না যে কোন ফিল্ড একটি রেকর্ডের পাসপোর্ট।
অ্যারেই বিভাজক রেখা
একটি প্রশ্ন করুন: এই অ্যারের ক্রম নিজেই ডেটা কি?
- ক্রম ডেটা: মাইগ্রেশন ধাপ, সার্চ র্যাঙ্কিং, চার্ট পয়েন্ট, অনুমোদন শৃঙ্খল। অবস্থান বদলানো আচরণ বদলানো, তাই অবস্থানভিত্তিক তুলনা সঠিক।
- ক্রম অর্থহীন: ট্যাগ, অনুমতি-ID সেট, অর্ডার লাইন আইটেম, ইনভেন্টরি তালিকা। একটি ইনসার্ট অন্য আইটেম কীভাবে বিচার হবে তা প্রভাবিত করা উচিত নয়।
ক্রম অর্থহীন হলে রেকর্ডকে অবস্থান দিয়ে পেটানোর বদলে একটি কি দিয়ে ইনডেক্স করুন:
// 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 দিয়ে নরমালাইজ
লাইব্রেরি না চাইলে আর কেবল দেখতে চাইলে আগে নরমালাইজ, তারপর ডিফ। 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জায়গায় সম্পাদনা করতে পারে না; স্ট্যান্ডার্ড আউটপুটে লেখে। প্রসেস সাবস্টিটিউশন (<(...)) ব্যবহার করুন, বা টেম্প ফাইলে লিখে যাচাই করে তারপর প্রতিস্থাপন করুন;-rস্ট্রিং থেকে কোট সরায়, তাই আউটপুটে নিউলাইন বা শেল-গুরুত্বপূর্ণ অক্ষর থাকতে পারে। অপরিচিত ডেটা সামনে পাইপ করার আগে কোট করুন।
এই সাইটের /diff কী করে, আর কী করে না
অন্তর্নির্মিত JSON তুলনা টুল একই কাঠামোগত পদ্ধতি অনুসরণ করে: দুই পক্ষকে ট্রিতে পার্স করে পাথ দিয়ে পুনরাবৃত্তভাবে মেলায়, path + type(add/remove/modify) + before/after দেয়। অবজেক্টের কি-ক্রম ফলাফল প্রভাবিত করে না, আর সবকিছু ব্রাউজারে চলে — পেস্ট করা পেললোড কখনো আপলোড হয় না।
তবে এর অ্যারে ইনডেক্স দিয়ে মেলা হয়। এটি সচেতন সমঝোতা, ত্রুটি নয়:
- শেষে যোগ, একই অবস্থান বদলানো, বা সম্পূর্ণ প্রতিস্থাপনে পরিষ্কার আউটপুট;
- শুরুতে বা মাঝখানে রেকর্ড ঢোকালে তার পরের সব
modifyহয়, আর আপনাকে বুঝতে হবে "আসলে কেবল একটি রেকর্ড নতুন"।
তাই অর্ডার তালিকা তুলনা করলে সঠিক পদক্ষেপ পেস্ট করার আগে দুই দিক id দিয়ে সাজানো। সাজানো ইনপুট ইনসার্ট পয়েন্ট পিন করে শব্দকে একটি এন্ট্রিতে নামিয়ে আনে। ইনস্টল দরকার নেই, আর দৈনন্দিন বেশিরভাগ ক্ষেত্র ঢাকে।
"প্রযোজ্য প্যাচ তৈরি" বা "সরানো এলিমেন্ট শনাক্ত" দরকার হলে পাতার টুল তা করে না। সেটি পরের অংশ।
ডিফকে প্যাচে বদলানো: দুই ফরম্যাট, মিশাবেন না
মানুষ ডিফ পড়ে; প্রোগ্রাম প্যাচ প্রয়োগ করে। JSON-এ দুই প্যাচ ফরম্যাট আছে সম্পূর্ণ ভিন্ন সেমান্টিক্স নিয়ে।
| মাত্রা | 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 ব্যর্থ হলে পুরো প্যাচ প্রত্যাখ্যাত। এটি প্রোটোকলে গাঁথা অপ্টিমিস্টিক কনকারেন্সি — "আমি কেবল সংস্করণ ৩ দেখে লিখেছি" নিশ্চিত করতে বাড়তি If-Match হেডার দরকার নেই। PATCH এন্ডপয়েন্টের জন্য এটি যেকোনো ডকুমেন্টেশন প্রথার চেয়ে বেশি মূল্যবান।
পাথ JSON Pointer (RFC 6901) ব্যবহার করে: ~0 ~ এস্কেপ করে, ~1 / এস্কেপ করে। স্ল্যাশযুক্ত কি (যেমন কি হিসেবে MIME টাইপ) এস্কেপ করতে হবে, নাহলে পয়েন্টার অন্য জায়গায় পড়ে। বিস্তারিত JSON Pointer ও Patch-এ।
একটি ব্যবহারিক সমস্যা থাকে: ডিফ থেকে স্বয়ংক্রিয়ভাবে তৈরি প্যাচ সাধারণত অ্যারেকে ইনডেক্স দিয়ে সম্বোধন করে। ইনডেক্স প্রোডাকশনে ভঙ্গুর — একটি সমবর্তী ইনসার্টেই /items/3 আর একই রেকর্ডে ইশারা করে না। হয় তৈরি প্যাচ অবজেক্ট ফিল্ডে সীমিত করুন, নয় প্রয়োগের আগে test দিয়ে গুরুত্বপূর্ণ ফিল্ড যাচাই করুন।
তুলনাকে CI-তে বসান: মান দাবি করুন, টেক্সট নয়
তিন নিয়মই যথেষ্ট:
- টেস্টে পার্স করা মান দাবি করুন, কখনো
JSON.stringify-এর আউটপুট নয়। Jest-এtoEqual; Python-এjson.loads-এর পরassert a == b। কি-ক্রম ও ইনডেন্টেশন অংশ নেয় না। - ডাইনামিক ফিল্ড হোয়াইটলিস্ট করুন।
requestId,timestamp,traceIdপ্রতিবার আলাদা; বাদ না দিলে টেস্ট স্থায়ীভাবে লাল। jsondiffpatch-এরpropertyFilterও deepdiff-এরexclude_pathsএজন্যই আছে। - ব্যর্থতায় পাথ ছাপান, পুরো ডকুমেন্ট নয়। লগে কেউ ৫ MB পেললোড পড়ে না;
$.data.items[id=ORD-042].qty: 2 → 3সঙ্গে সঙ্গে সমস্যা খুঁজে দেয়।
কনফিগ ড্রিফটও একইভাবে: স্টেজিং ও প্রোডাকশন কনফিগ দুইটিই jq -S করুন, অ্যারে ব্যবসায়িক কি দিয়ে সাজান, আর যা থাকে তা প্রকৃত পার্থক্য। এই শ্রেণির সমস্যা — একই ডেটা পরিবেশভেদে আলাদা দেখা — সাধারণত অনির্ধারিত চুক্তি, যা API চুক্তি নকশা-এর বিষয়।
সাধারণ প্রশ্ন
কি-ক্রম কি পার্থক্য হিসেবে গোনা হয়?
না। RFC 8259 অবজেক্টকে ক্রমহীন সংগ্রহ সংজ্ঞায়িত করে, তাই {"a":1,"b":2} ও {"b":2,"a":1} একই মান। পুনর্বিন্যাস পরিবর্তন রিপোর্ট করা যেকোনো টুল টেক্সট তুলনা করছে, JSON তুলনা নয়।
একটি রেকর্ড ঢোকালে প্রতিটি এন্ট্রি বদলানো দেখায় কেন?
কারণ টুল অ্যারে ইনডেক্স-ধরে-ইনডেক্স মেলায়। নতুন এলিমেন্ট অবস্থান ০-এ বসে, প্রতিটি বিদ্যমান রেকর্ড এক নিচে সরে, আর কোনো দুই অবস্থান মেলে না। অবস্থানভিত্তিক না করে তুলনার আগে id-এর মতো কি দিয়ে সাজান বা ইনডেক্স করুন।
অনলাইন তুলনা টুল কি আমার ডেটা আপলোড করে?
বাস্তবায়নের উপর নির্ভর করে। সার্ভার-রেন্ডারড টুল পেললোড ব্যাকএন্ডে পাঠায়, যেখানে তা লগ হতে পারে; ক্লায়েন্ট-পক্ষের টুল ব্রাউজারে পার্স করে, কিছুই আপলোড করে না। প্রোডাকশন কনফিগ বা ব্যবহারকারীর পেললোড তুলনার আগে নিশ্চিত করুন পেজ স্থানীয়ভাবে গণনা করে, বা নেটওয়ার্ক বিচ্ছিন্ন করে আবার চেষ্টা করুন।
ডিফ ফল সোজা প্যাচ হিসেবে ব্যবহার করা যায়?
সোজা নয়। ডিফ বলে কোথায় আলাদা; JSON Patch বলে কোন অপারেশন ক্রমে চালাতে হবে। তাদের মাঝে একটি রূপান্তর স্তর বসে। তৈরি প্যাচও অ্যারেকে ইনডেক্স দিয়ে সম্বোধন করে, যা সমবর্তী ইনসার্টে ভাঙে — প্রয়োগের আগে test অপারেশন যোগ করুন।
কয়েক ডজন মেগাবাইটের ফাইল কীভাবে তুলনা করব?
আগে স্ট্রিম করুন (jq -c প্রতি লাইনে একটি রেকর্ড লেখে), বা কেবল দরকারি সাবট্রি বের করুন, যেমন jq '.data.items'। কয়েক ডজন মেগাবাইটের পূর্ণ পুনরাবৃত্ত তুলনা তবু মেমরিতে ফিট করে; শত মেগাবাইটের পরে টুকরো করুন বা কি দিয়ে ইনডেক্স করুন। নির্দিষ্ট মেমরি ও সময়ের সংখ্যার জন্য দেখুন বড় JSON পারফরম্যান্স।
তুলনার আগে অ্যারে id দিয়ে সাজানো উচিত?
হ্যাঁ। অন্তর্নির্মিত JSON তুলনা টুল অ্যারে ইনডেক্স দিয়ে মেলায়, তাই শুরুতে বা মাঝখানে একটি রেকর্ড ঢোকালে তার পরের সব modify হয়; পেস্ট করার আগে দুই দিক id দিয়ে সাজালে ইনসার্ট পয়েন্ট পিন হয় আর শব্দ একটি এন্ট্রিতে নামে। ব্যতিক্রম হলো অ্যারের ক্রম নিজেই ডেটা — মাইগ্রেশন ধাপ, সার্চ র্যাঙ্কিং, চার্ট পয়েন্ট — যেখানে অবস্থানভিত্তিক তুলনা সঠিক আর সাজানো ভুল।