การเลือกเครื่องมือ JSON diff: text diff, structural diff และการแสดงภาพ
สรุปสำคัญ: Text diff ค้นหาว่าไฟล์ต่างกันตรงไหน ส่วน structural diff ตัดสินว่าเนื้อหาเทียบเท่ากันหรือไม่ การเลือกระดับให้ถูกต้องช่วยไม่ให้ลำดับคีย์ถูกเข้าใจผิดว่าเป็นเหตุการณ์ผิดพลาด

สรุปสั้น: text diff ค้นหาการเปลี่ยนแปลง (จัดเรียงคีย์ด้วย jq -S ก่อน), structural diff ตัดสินความเทียบเท่า (จับคู่ array ด้วย primary key) และการแสดงภาพมีหน้าที่เพียงอธิบายหรือรีวิวเท่านั้น ให้แยกทั้งสามชั้นออกจากกันใน CI
เอกสาร JSON สองฉบับที่ดูต่างกันไม่ได้หมายความว่าเนื้อหาต่างกัน ให้เลือกระดับการเปรียบเทียบให้ถูกก่อนเลือกเครื่องมือ
1. "diff" สามแบบไม่ใช่เรื่องเดียวกัน
หลายทีมมอง "เปรียบเทียบ JSON" เป็นการกระทำเดียว แล้วก็เจอปัญหาเดิมซ้ำในสถานการณ์ต่าง ๆ การแยกเป็นสามชั้นช่วยได้:
| ชั้น | เปรียบเทียบอะไร | เผยให้เห็นอะไร | การใช้งานทั่วไป |
|---|---|---|---|
| Text diff | ไบต์และบรรทัดดิบ | การย่อหน้า, ลำดับคีย์, ช่องว่าง, ขึ้นบรรทัดใหม่ | configuration drift, การรีวิวโค้ด |
| Structural diff | object และ array ที่ parse แล้ว | คีย์ที่เพิ่มหรือลบ, ค่าที่เปลี่ยน, การเปลี่ยนแปลงของสมาชิก array | API regression, การย้ายข้อมูล |
| Visual diff | วิธีนำเสนอความแตกต่าง | การเปลี่ยนแปลงอยู่ระดับไหน, ขอบเขตผลกระทบกว้างแค่ไหน | การรีวิวโดยมนุษย์, การคุยกันในทีม |
Text diff ไม่มี dependency, เร็ว และผสานกับ Git ได้อย่างเป็นธรรมชาติ จุดอ่อนคือมันไม่รู้อะไรเกี่ยวกับความหมายของ JSON เอกสารสองฉบับที่เทียบเท่ากันสมบูรณ์จะขึ้นสีแดงและเขียวถ้าลำดับคีย์หรือการย่อหน้าต่างกัน Structural diff ตรงกันข้าม: มัน parse ก่อนแล้วจึงเปรียบเทียบ จึงรู้ว่าค่าเพียงแค่ย้ายที่ แต่ต้องใช้กลยุทธ์จับคู่ array ที่ถูกต้อง ไม่เช่นนั้นการแทรกครั้งเดียวจะถูกอ่านเป็นการเขียนใหม่ทั้งหมด
การแสดงภาพไม่ใช่อัลกอริทึมที่สาม มันแสดงผลลัพธ์ของสองชั้นข้างต้นเพื่อให้คนยืนยันได้อย่างรวดเร็วว่าความแตกต่างนั้นคาดหมายไว้หรือไม่ มันดีสำหรับการตัดสินของมนุษย์และแย่มากเมื่อใช้แทนการตรวจอัตโนมัติ การผสมชั้นต่าง ๆ เข้าด้วยกันมักได้ CI ที่แดงตลอดและทีมที่เรียนรู้ที่จะเพิกเฉยต่อมัน
2. Text diff: จัดระเบียบลำดับคีย์ด้วย jq -S ก่อน
สำหรับการดูสองไฟล์อย่างรวดเร็ว ชุดค่าผสมที่ประหยัดที่สุดคือ jq -S บวก diff -u แฟล็ก -S จะเรียงคีย์ของ object ใหม่แบบเรียกซ้ำ ทำให้เอกสารที่เนื้อหาเหมือนกันแต่ลำดับคีย์ต่างกันถูกปรับให้เป็นมาตรฐาน:
jq -S . a.json > a.sorted.json
jq -S . b.json > b.sorted.json
diff -u a.sorted.json b.sorted.json
วิธีนี้กำจัด false positive ที่พบบ่อยที่สุด RFC 8259 นิยาม object ว่าเป็นคอลเลกชันที่ไม่มีลำดับ ดังนั้น {"a":1,"b":2} กับ {"b":2,"a":1} จึงเป็นค่าเดียวกัน แต่ text diff กลับทำเครื่องหมายทุกบรรทัดว่าถูกเปลี่ยน การจัดเรียงก่อนทำให้บล็อกสีแดงใกล้เคียงกับปัญหาจริงมากขึ้น
สังเกตว่า jq -S เรียงเฉพาะคีย์ ไม่ได้เรียง array ลำดับของ array มีความหมายใน JSON และ [1,2] ไม่ใช่ค่าเดียวกับ [2,1] เครื่องมือจึงไม่ตัดสินใจแทนคุณ หาก array ในเชิงแนวคิดเป็นเซตที่ไม่มีลำดับ ให้เรียงตาม primary key เองก่อนเปรียบเทียบ
มีอีกรายละเอียดที่ควรจำ: diff ออกด้วยรหัส 1 เมื่อพบความแตกต่าง แต่ไฟล์ที่หายไปหรือ error ด้านสิทธิ์ก็ให้ exit code ที่ไม่เป็นศูนย์เช่นกัน ใน CI อย่าถือว่ารหัสที่ไม่เป็นศูนย์ทุกตัวหมายถึง "เนื้อหาเปลี่ยน" ไม่อย่างนั้น path ที่พิมพ์ผิดจะถูกรายงานว่าเป็นข้อมูลที่เปลี่ยน รูปแบบที่ปลอดภัยกว่าคือตรวจสอบไฟล์ให้แน่ใจก่อน แล้วจึงจัดการกับความแตกต่าง:
if ! diff -u a.sorted.json b.sorted.json; then
echo "JSON content differs" >&2
exit 1
fi
delta ทำให้ text diff อ่านง่าย
เอาต์พุต diff -u ดิบใช้ได้ แต่บรรทัดยาว ๆ และ JSON ที่ซ้อนลึกอ่านแล้วเหนื่อย delta เป็นตัวกรอง diff ที่เพิ่มการไฮไลต์ไวยากรณ์, เลขบรรทัด และสีพื้นหลังที่ชัดขึ้น:
diff -u a.sorted.json b.sorted.json | delta
มันเริ่มต้นชีวิตในฐานะ pager ของ Git จึงสามารถรับช่วง git diff ได้ด้วย สำหรับคนที่อ่าน diff ของไฟล์กำหนดค่าสิบฉบับต่อวัน เครื่องมือแบบนี้ช่วยเพิ่มความเร็วในการอ่านมากกว่าความแม่นยำเชิงอัลกอริทึม อีกตัวเลือกที่พบบ่อยคือ git diff --no-index --word-diff ซึ่งเปรียบเทียบระดับคำและใช้ได้ดีกับ JSON แบบบีบอัดที่เก็บในบรรทัดยาวมาก ๆ
ข้อควรระวังหนึ่งข้อ: การทำให้สวยไม่ได้กำจัดความแตกต่างปลอม หากลำดับคีย์ยังไม่ถูกปรับให้เป็นมาตรฐาน delta เพียงแต่ทำให้เสียงรบกวนสวยขึ้นเท่านั้น ลำดับที่ถูกต้องคือปรับให้เป็นมาตรฐานก่อน ตกแต่งทีหลัง
3. Structural diff: jsondiffpatch เข้าใจความหมาย
เมื่อคำถามคือข้อมูลสองชิ้นเทียบเท่ากันในเชิงความหมายหรือไม่ ให้ใช้ structural diff ตัวเลือกที่พบบ่อยที่สุดในระบบนิเวศ JavaScript คือ jsondiffpatch ซึ่งคืน delta object ที่อธิบายการเปลี่ยนแปลง แทนที่จะเป็นบรรทัดข้อความที่ทำเครื่องหมาย + และ -:
import { create } from 'jsondiffpatch';
const instance = create();
const delta = instance.diff(
{ id: 1, tags: ['a', 'b'], enabled: true },
{ id: 1, tags: ['a', 'c'], enabled: false },
);
console.log(JSON.stringify(delta, null, 2));
delta แยกแยะคีย์ที่เพิ่ม, คีย์ที่ลบ, ค่าที่ถูกแทนที่ และสมาชิก array ที่ย้ายที่ นั่นหมายความว่าคุณเดินดู delta ได้โดยตรงใน assertion: undefined เมื่อไม่มีความแตกต่าง หรือคำอธิบายทีละ path เมื่อมีความแตกต่าง ต่างจาก text diff มันไม่ถูกรบกวนจากการย่อหน้า, ลำดับคีย์ หรือการเขียนจำนวนแบบ 1e3 กับ 1000
สิ่งที่สำคัญพอกันคือเอาต์พุตของ structural diff ป้อนเข้าสู่กระบวนการ patch ได้ jsondiffpatch มี patch และ reverse ดังนั้น delta สามารถนำไปใช้กับ object ต้นฉบับและย้อนกลับภายหลังได้ ข้อควรระวัง: delta ที่สร้างอัตโนมัติไม่ใช่รูปแบบเดียวกับ JSON Patch ที่เขียนด้วยมือ และการแปลงระหว่างกันต้องใช้ความพยายาม สำหรับแนวทางการเปรียบเทียบที่กว้างกว่า ดู JSON diff และการเปรียบเทียบ
4. วิธีกำจัดความแตกต่างปลอมที่เกิดจากลำดับคีย์
ลำดับคีย์ไม่เพียงน่ารำคาญ แต่ยังทำให้ได้ข้อสรุปผิดได้ ทำสามขั้นตอนเมื่อสอบสวน:
- ก่อนเปรียบเทียบ ปรับลำดับคีย์ให้เป็นมาตรฐานแบบเรียกซ้ำด้วย
jq -Sหรือเครื่องมือที่เทียบเท่า - ตัดฟิลด์ที่เปลี่ยนทุกครั้งที่รัน เช่น
timestamp,requestIdและtraceId - ยืนยันว่าไม่มีคีย์ซ้ำ คีย์ซ้ำจะถูกแก้ด้วยกฎ "ตัวหลังชนะ" ระหว่าง parse ดังนั้น text diff เห็นมันแต่ structural diff ไม่เห็น
jq -S 'del(.meta.timestamp, .meta.requestId)' a.json > /tmp/a.json
jq -S 'del(.meta.timestamp, .meta.requestId)' b.json > /tmp/b.json
diff -u /tmp/a.json /tmp/b.json
ลำดับงานนี้เหมาะกับการตรวจ configuration drift ให้รัน staging และ production ผ่านการปรับมาตรฐานเดียวกัน แล้วสิ่งที่เหลือคือความแตกต่างที่ต้องอธิบายจริง ๆ เก็บกฎการกรองไว้ในสคริปต์ที่ควบคุมเวอร์ชันแทนการพิมพ์ใหม่จากความจำ สำหรับด้านการแก้ไขและ scripting ของ toolchain ดู Editor, command line และ automation
5. Array: จับคู่ตาม index หรือตาม id?
Array คือส่วนที่เกิดข้อผิดพลาดง่ายที่สุดของการเปรียบเทียบ JSON ด้วยการจับคู่ตาม index แบบค่าเริ่มต้น การแทรกเรกคอร์ดเดียวที่ต้นจะเลื่อนสมาชิกทุกตัวหลังจากนั้น และเครื่องมือจะทำเครื่องหมายว่าทั้งหมดเปลี่ยน เครื่องมือไม่ได้เสีย แต่กลยุทธ์การจับคู่ผิด
| การจับคู่ | ใช้ได้เมื่อ | จุดแข็ง | ความเสี่ยง |
|---|---|---|---|
| ตาม index | ลำดับ array มีความหมายทางธุรกิจ | เรียบง่าย ไม่ต้องมีฟิลด์เพิ่ม | การแทรกครั้งเดียวทำให้เกิด false positive เป็นทอด ๆ |
| ตาม primary key | สมาชิกเป็นเซตที่มี id ไม่ซ้ำ | ตรวจจับการแทรก, การลบ และการย้ายได้ | ต้องมีคีย์ที่ไม่ซ้ำและเสถียร |
| Longest common subsequence | ลำดับมีความหมายแต่ยอมให้แทรก | เสถียรกว่าการใช้ index ล้วน | อธิบายยากกว่าและมีต้นทุนสูงกว่า |
jsondiffpatch ใช้ objectHash เพื่อประกาศ primary key โดยจัด index สมาชิก array ก่อนเปรียบเทียบ:
const instance = create({
objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});
กับ jq ธรรมดา คุณสามารถเรียงก่อนแล้วใช้ text diff:
jq -S 'sort_by(.id)' items.json > items.sorted.json
ข้อแลกเปลี่ยนของการจับคู่ด้วย id คือการเปลี่ยนตำแหน่งไม่นับเป็นความแตกต่างอีกต่อไป หากธุรกิจต้องรู้ว่าเรกคอร์ดย้ายจากตำแหน่ง 2 ไปตำแหน่ง 5 ให้เปรียบเทียบฟิลด์ลำดับอย่างชัดเจนด้วย อย่าพึ่งพาเนื้อหาของเซตเพียงอย่างเดียว
6. Visual diff: ชั้นที่สร้างมาเพื่อมนุษย์
เอาต์พุตของ structural diff ถูกออกแบบมาสำหรับเครื่อง ไม่ใช่สำหรับการประชุม เมื่อคุณต้องอธิบายการเปลี่ยนแปลงให้คนอื่น การแสดงภาพได้ผลกว่า:
- diff editor ของ VS Code เหมาะกับการรีวิวทีละไฟล์ ด้วยบานหน้าต่างเคียงข้างกันและไฮไลต์แบบอินไลน์
- tree viewer อย่าง JSON Crack และ JSON Hero แสดงว่าการเปลี่ยนแปลงอยู่ระดับไหน
jsondiffpatchมาพร้อม HTML formatter ที่แสดง delta เป็นมุมมองแบบพับเก็บได้
คุณค่าของการแสดงภาพคือบริบท text diff บอกว่าบรรทัด 88 เปลี่ยน ส่วน tree view บอกว่าบรรทัด 88 อยู่ใต้ data.items[3].price แต่การแสดงภาพมักโหลดเอกสารทั้งหมดเข้า browser หรือแอปเดสก์ท็อป ก่อนเปิด production payload ให้ยืนยันว่าเครื่องมือคำนวณในเครื่องหรืออัปโหลดไปยังเซิร์ฟเวอร์
7. แนวปฏิบัติสามข้อในการนำ diff เข้า CI
ข้อแรก ปรับมาตรฐานก่อนเปรียบเทียบ กำหนดการย่อหน้าให้แน่นอน เรียงคีย์แบบเรียกซ้ำ และกรองฟิลด์ที่ผันผวนก่อนที่ diff จะรัน ไม่อย่างนั้น CI จะล้มเหลวซ้ำ ๆ จากเสียงรบกวน
ข้อสอง เลือกการจับคู่ตามความหมาย หาก array เป็นเซต ให้จับคู่ตาม primary key หากเป็นลำดับ ให้คงการจับคู่ตาม index อย่าใช้กฎเดียวกับทุก API
ข้อสาม ตรวจสอบค่า ไม่ใช่ข้อความ การเปรียบเทียบ object ที่ parse แล้วใน test เสถียรกว่าการเปรียบเทียบเอาต์พุตของ JSON.stringify มาก เมื่อการตรวจล้มเหลว ให้พิมพ์ path ของความแตกต่างและค่าก่อน/หลัง แทนการทิ้ง payload ทั้งก้อนลง log
# Print only the differing paths and values, not the entire document
jq -S 'del(.timestamp)' actual.json > /tmp/actual.json
jq -S 'del(.timestamp)' expected.json > /tmp/expected.json
diff -u /tmp/expected.json /tmp/actual.json | head -n 80
หากความแตกต่างต้องติดตามในระยะยาว ให้ commit สคริปต์ปรับมาตรฐานและไฟล์ expected ไปด้วยกัน หากเป็นการสอบสวนครั้งเดียว ให้ลบสคริปต์ชั่วคราวทิ้งภายหลัง เพื่อไม่ให้มันกลายเป็น implementation ที่สองของตรรกะธุรกิจอย่างเงียบ ๆ
8. ตารางเลือกเครื่องมือ
| สถานการณ์ | ตัวเลือกแรก | ทางเลือกอื่น | ข้อควรจำ |
|---|---|---|---|
| เปรียบเทียบไฟล์เป็นครั้งคราว | VS Code diff | diff -u |
จัดรูปแบบก่อนเพื่อกำจัดเสียงรบกวน |
| Configuration drift | jq -S + diff |
delta |
กรองฟิลด์ที่ผันผวน |
| API response regression | jsondiffpatch |
deepdiff |
จับคู่ array ตาม primary key |
| อธิบายการเปลี่ยนแปลงให้ทีม | JSON Crack / JSON Hero | HTML formatter | ตรวจว่าข้อมูลถูกอัปโหลดหรือไม่ |
| ไฟล์ขนาดใหญ่มาก | ดึง subtree ด้วย jq |
เรียกดูด้วย jless |
อย่าโหลดทั้ง tree เข้าหน่วยความจำ |
| ตรวจก่อน commit | git diff --no-index |
pre-commit | จัดการ exit code อย่างชัดเจน |
ไม่มีเครื่องมือ JSON diff ที่ดีที่สุดเพียงตัวเดียว มีเพียงเครื่องมือที่เหมาะกับชั้นปัจจุบัน Text diff ค้นหาการเปลี่ยนแปลง structural diff ตัดสินความเทียบเท่า และการแสดงภาพอธิบายการเปลี่ยนแปลง การแยกทั้งสามชั้นออกจากกันเชื่อถือได้มากกว่าการหาเครื่องมือเดียวที่ทำได้ทุกอย่าง สำหรับการจัดการความขัดแย้งข้าม branch ให้อ่านต่อที่ JSON และ Git merge
คำถามที่พบบ่อย
jq -S เปลี่ยนลำดับ array ด้วยหรือไม่?
ไม่ -S เรียงเฉพาะคีย์ของ object และปล่อย array ตามลำดับเดิม Array มีลำดับใน JSON เครื่องมือจึงไม่เปลี่ยน [1,2] เป็น [2,1] หาก array ในเชิงแนวคิดเป็นเซตที่ไม่มีลำดับ ให้เรียงอย่างชัดเจนด้วย sort_by
ใน structural diff ค่า 1 กับ 1.0 เป็นค่าเดียวกันหรือไม่?
parser ส่วนใหญ่แปลงทั้งสองเป็นจำนวนเดียวกัน ดังนั้น structural diff มักถือว่าเท่ากัน ขณะที่ text diff ทำเครื่องหมายว่าต่างกัน ใช้ structural diff เพื่อตัดสินความเทียบเท่า และใช้ text diff เพื่อตัดสินว่าไฟล์เหมือนกันทุกไบต์หรือไม่
ใช้เอาต์พุตของ diff เป็น JSON Patch ได้โดยตรงหรือไม่?
ไม่ได้โดยตรง เครื่องมือ diff รายงานว่าอะไรต่างกัน ขณะที่ JSON Patch อธิบายรายการ operation ตามลำดับ รูปแบบและความหมายต่างกัน patch ที่สร้างอัตโนมัติมักอ้าง index ของ array ซึ่งล้าสมัยภายใต้การทำงานพร้อมกัน ให้เพิ่ม operation test ก่อนนำไปใช้
ควรเปรียบเทียบไฟล์ JSON ขนาดหลายสิบเมกะไบต์อย่างไร?
ดึง subtree ที่สนใจออกมาก่อน เช่นด้วย jq '.data.items' แล้วเรียงตาม primary key และเปรียบเทียบ อย่าโหลดทั้ง tree เข้าหน่วยความจำเพื่อทำ diff แบบเรียกซ้ำ ไฟล์ขนาดใหญ่มากจัดการได้ดีกว่าด้วยการเปรียบเทียบแบบ streaming หรือแบบแบ่งก้อน และต้องประเมินต้นทุนหน่วยความจำแยกต่างหาก
เครื่องมือ diff ออนไลน์ทำข้อมูลรั่วได้หรือไม่?
ขึ้นอยู่กับการนำไปใช้ เครื่องมือแบบ front-end ล้วนคำนวณใน browser และไม่อัปโหลดข้อมูล ส่วนเครื่องมือที่ render ฝั่งเซิร์ฟเวอร์ส่งเนื้อหาไปยัง backend ซึ่งอาจเข้าไปอยู่ใน log ก่อนเปรียบเทียบ production configuration หรือ payload ของลูกค้า ให้ยืนยันว่าหน้านั้นคำนวณในเครื่อง และทดสอบสักครั้งด้วยการตัดการเชื่อมต่อเครือข่ายหากจำเป็น
จะตัดสินใจเลือกระหว่าง text diff กับ structural diff อย่างไร?
สำหรับการดูอย่างเร็วว่าไฟล์สองไฟล์ต่างกันตรงไหน ให้เรียงคีย์ด้วย jq -S แล้วรัน diff -u หากต้องการตัดสินว่าเอกสารสองฉบับเทียบเท่ากันในเชิงความหมาย ให้ใช้ structural diff เช่น jsondiffpatch และจับคู่ array ตาม primary key Text diff ไม่มี dependency และผสานกับ Git ได้ ส่วน structural diff ไม่ถูกรบกวนจากการย่อหน้า, ลำดับคีย์ หรือการเขียนจำนวน
ทำไม CI ไม่ควรถือทุก exit code ที่ไม่เป็นศูนย์ของ diff ว่าเป็นความแตกต่าง?
diff ออกด้วยรหัส 1 เมื่อพบความแตกต่าง แต่ไฟล์ที่หายไปหรือ error ด้านสิทธิ์ก็คืนค่าที่ไม่เป็นศูนย์เช่นกัน การถือทุกรหัสที่ไม่เป็นศูนย์ว่า "เนื้อหาเปลี่ยน" จะรายงาน path ที่พิมพ์ผิดว่าเป็นข้อมูลที่เปลี่ยน รูปแบบที่ปลอดภัยกว่าคือตรวจสอบไฟล์ก่อนแล้วจัดการกับความแตกต่างแยกออกมา
ควรทำ diff ไฟล์ขนาดหลายสิบเมกะไบต์ใน browser หรือไม่?
ไม่ ให้ดึง subtree ที่สนใจด้วย jq ก่อน แล้วเรียงตาม primary key และเปรียบเทียบ การโหลดทั้ง tree เข้าหน่วยความจำเพื่อทำ diff แบบเรียกซ้ำใช้หน่วยความจำและเวลามากกว่ามาก ไฟล์ขนาดใหญ่มากจัดการได้ดีกว่าด้วยการเปรียบเทียบแบบ streaming หรือแบบแบ่งก้อน