Chọn công cụ JSON diff: diff văn bản, diff cấu trúc và trực quan hoá
Kết luận: Diff văn bản tìm ra nơi tệp khác nhau; diff cấu trúc quyết định nội dung có tương đương không. Chọn đúng lớp giúp thứ tự khóa không bị nhầm thành sự cố.

Kết luận: diff văn bản phát hiện thay đổi (sắp xếp khóa bằng jq -S trước), diff cấu trúc quyết định tương đương (căn mảng theo khóa chính), và trực quan hoá chỉ giải thích hoặc review; hãy tách ba lớp này trong CI.
Hai tài liệu JSON trông khác nhau không có nghĩa nội dung của chúng khác nhau. Hãy chọn đúng lớp so sánh trước khi chọn công cụ.
1. Ba loại "diff" không phải là một
Nhiều nhóm coi "so sánh JSON" là một thao tác duy nhất, rồi vấp cùng những vấn đề trong các kịch bản khác nhau. Sẽ hữu ích khi tách thành ba lớp:
| Lớp | So sánh điều gì | Nó phơi bày điều gì | Dùng điển hình |
|---|---|---|---|
| Diff văn bản | Byte và dòng thô | Thụt lề, thứ tự khóa, khoảng trắng, xuống dòng | Lệch cấu hình, review mã |
| Diff cấu trúc | Object và mảng đã phân tích | Khóa thêm hoặc bớt, giá trị thay đổi, thay đổi phần tử mảng | Hồi quy API, di chuyển dữ liệu |
| Diff trực quan | Cách trình bày khác biệt | Thay đổi thuộc cấp nào, bán kính ảnh hưởng lớn đến đâu | Review của con người, thảo luận nhóm |
Diff văn bản không phụ thuộc gì, nhanh, và tích hợp tự nhiên với Git. Điểm yếu là nó không biết gì về ngữ nghĩa JSON. Hai tài liệu hoàn toàn tương đương sẽ sáng đỏ xanh nếu thứ tự khóa hoặc thụt lề khác nhau. Diff cấu trúc thì ngược lại: nó phân tích trước rồi mới so sánh, nên nhận ra rằng giá trị chỉ đơn giản là đã di chuyển. Nhưng nó cần chiến lược căn mảng đúng; nếu không, một lần chèn sẽ bị đọc thành viết lại toàn bộ.
Trực quan hoá không phải thuật toán thứ ba. Nó render đầu ra của hai lớp kia để con người nhanh chóng xác nhận khác biệt có như mong đợi không. Nó tốt cho phán đoán của con người và tệ khi thay thế kiểm tra tự động. Trộn các lớp với nhau thường tạo ra một pipeline CI đỏ vĩnh viễn và một nhóm đã học cách lờ nó đi.
2. Diff văn bản: làm sạch thứ tự khóa bằng jq -S trước
Để xem nhanh hai tệp, tổ hợp rẻ nhất là jq -S cộng diff -u. Cờ -S sắp xếp khóa object một cách đệ quy, chuẩn hoá những tài liệu có nội dung giống nhau nhưng thứ tự khóa khác nhau:
jq -S . a.json > a.sorted.json
jq -S . b.json > b.sorted.json
diff -u a.sorted.json b.sorted.json
Cách này loại bỏ false positive phổ biến nhất. RFC 8259 định nghĩa object là tập hợp không có thứ tự, nên {"a":1,"b":2} và {"b":2,"a":1} là cùng một giá trị. Tuy nhiên diff văn bản lại đánh dấu mọi dòng là thay đổi. Sắp xếp trước khiến các khối đỏ gần với vấn đề thật hơn nhiều.
Lưu ý jq -S chỉ sắp xếp khóa; nó không sắp xếp mảng. Thứ tự mảng có ý nghĩa trong JSON, và [1,2] không phải cùng giá trị với [2,1], nên công cụ sẽ không ra quyết định đó thay bạn. Nếu một mảng về mặt khái niệm là tập hợp không thứ tự, hãy tự sắp xếp nó theo khóa chính trước khi so sánh.
Còn một chi tiết đáng nhớ: diff thoát với mã 1 khi tìm thấy khác biệt, nhưng tệp bị thiếu hoặc lỗi quyền cũng tạo mã thoát khác 0. Trong CI, đừng coi mọi mã khác 0 là "nội dung đã thay đổi", nếu không một đường dẫn gõ sai sẽ bị báo là thay đổi dữ liệu. Một mẫu an toàn hơn là xác minh tệp trước rồi mới xử lý khác biệt:
if ! diff -u a.sorted.json b.sorted.json; then
echo "JSON content differs" >&2
exit 1
fi
delta giúp diff văn bản dễ đọc
Đầu ra diff -u thô vẫn dùng được, nhưng dòng dài và JSON lồng sâu rất mỏi mắt. delta là một bộ lọc diff thêm highlight cú pháp, số dòng và màu nền rõ ràng hơn:
diff -u a.sorted.json b.sorted.json | delta
Nó khởi đầu là một pager của Git, nên cũng có thể thay thế git diff. Với những người đọc mười bản diff cấu hình mỗi ngày, các công cụ như vậy cải thiện tốc độ đọc chứ không phải độ chính xác thuật toán. Một lựa chọn phổ biến khác là git diff --no-index --word-diff, so sánh ở mức từ và rất hợp với JSON nén được lưu trên những dòng rất dài.
Một cảnh báo: làm đẹp không loại bỏ khác biệt giả. Nếu thứ tự khóa chưa được chuẩn hoá, delta chỉ làm nhiễu trông đẹp hơn. Thứ tự đúng luôn là chuẩn hoá trước, trang trí sau.
3. Diff cấu trúc: jsondiffpatch hiểu ngữ nghĩa
Khi câu hỏi là liệu hai mẩu dữ liệu có tương đương về ngữ nghĩa không, hãy dùng diff cấu trúc. Lựa chọn phổ biến nhất trong hệ sinh thái JavaScript là jsondiffpatch, nó trả về một object delta mô tả thay đổi thay vì những dòng văn bản có dấu + và -:
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 phân biệt khóa được thêm, khóa bị bớt, giá trị bị thay thế và phần tử mảng bị di chuyển. Nghĩa là bạn có thể đi thẳng trong delta ở assertion: undefined khi không có khác biệt, hoặc mô tả từng đường dẫn khi có. Khác với diff văn bản, nó không bị nhiễu bởi thụt lề, thứ tự khóa, hay cách viết số như 1e3 so với 1000.
Quan trọng không kém, đầu ra diff cấu trúc có thể nuôi một quy trình patching. jsondiffpatch cung cấp patch và reverse, nên một delta có thể áp lên object gốc và hoàn tác về sau. Một lưu ý: delta sinh tự động không cùng định dạng với JSON Patch viết tay, và chuyển đổi giữa chúng cần công sức. Với cách so sánh rộng hơn, xem JSON diff và so sánh.
4. Cách loại bỏ khác biệt giả do thứ tự khóa
Thứ tự khóa không chỉ khó chịu; nó có thể dẫn tới kết luận sai. Hãy theo ba bước khi điều tra:
- Trước khi so sánh, chuẩn hoá thứ tự khóa một cách đệ quy bằng
jq -Shoặc công cụ tương đương. - Loại bỏ những trường thay đổi mỗi lần chạy, như
timestamp,requestIdvàtraceId. - Xác nhận không có khóa trùng. Khóa trùng được giải quyết theo "cái cuối thắng" khi phân tích, nên diff văn bản thấy chúng còn diff cấu trúc thì không.
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
Luồng này rất hợp với lệch cấu hình. Chạy staging và production qua cùng một phép chuẩn hoá, và những gì còn lại chính là khác biệt thực sự cần giải thích. Hãy giữ quy tắc lọc trong một script có phiên bản thay vì gõ lại từ trí nhớ. Với phía chỉnh sửa và scripting của chuỗi công cụ, xem Trình soạn thảo, dòng lệnh và tự động hoá.
5. Mảng: căn theo chỉ số hay theo id?
Mảng là phần dễ lỗi nhất của việc so sánh JSON. Với cách căn theo chỉ số mặc định, chèn một bản ghi ở đầu sẽ đẩy mọi phần tử phía sau, và công cụ đánh dấu tất cả là thay đổi. Công cụ không hỏng; chiến lược căn mới là sai.
| Cách căn | Hiệu quả khi | Điểm mạnh | Rủi ro |
|---|---|---|---|
| Theo chỉ số | Thứ tự mảng mang ý nghĩa nghiệp vụ | Đơn giản, không cần thêm trường | Một lần chèn gây chuỗi false positive |
| Theo khóa chính | Phần tử là tập hợp có id duy nhất | Phát hiện chèn, xoá và di chuyển | Cần khóa ổn định và duy nhất |
| Chuỗi con chung dài nhất | Thứ tự quan trọng nhưng cho phép chèn | Bền hơn so với căn chỉ số thuần | Khó giải thích hơn, chi phí cao hơn |
jsondiffpatch dùng objectHash để khai báo khóa chính, đánh chỉ số phần tử mảng trước khi so sánh:
const instance = create({
objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});
Với jq thuần, bạn có thể sắp xếp trước rồi dùng diff văn bản:
jq -S 'sort_by(.id)' items.json > items.sorted.json
Đánh đổi của việc căn theo id là thay đổi vị trí không còn được tính là khác biệt. Nếu nghiệp vụ cần biết một bản ghi đã chuyển từ vị trí 2 sang vị trí 5, hãy so sánh thêm một trường thứ tự tường minh; đừng chỉ dựa vào nội dung tập hợp.
6. Diff trực quan: lớp dành cho con người
Đầu ra diff cấu trúc được thiết kế cho máy, không phải cho cuộc họp. Khi bạn cần giải thích một thay đổi cho người khác, trực quan hoá hiệu quả hơn:
- VS Code diff editor tốt cho review từng tệp, với hai khung cạnh nhau và highlight trong dòng.
- Các trình xem dạng cây như JSON Crack và JSON Hero cho thấy thay đổi thuộc cấp nào.
jsondiffpatchcó sẵn một formatter HTML render delta thành dạng xem phong phú có thể thu gọn.
Giá trị của trực quan hoá là ngữ cảnh. Diff văn bản nói dòng 88 đã thay đổi; một tree view nói dòng 88 thuộc về data.items[3].price. Nhưng trực quan hoá thường nạp toàn bộ tài liệu vào trình duyệt hoặc ứng dụng desktop. Trước khi mở một payload production, hãy xác nhận công cụ tính toán cục bộ hay tải lên máy chủ.
7. Ba thực hành để đưa diff vào CI
Thứ nhất, chuẩn hoá trước khi so sánh. Ghim thụt lề, sắp xếp khóa đệ quy, và lọc trường biến động trước khi diff chạy. Nếu không, CI sẽ thất bại lặp lại vì nhiễu.
Thứ hai, chọn cách căn dựa trên ngữ nghĩa. Nếu mảng là tập hợp, căn theo khóa chính. Nếu là chuỗi có thứ tự, giữ căn theo chỉ số. Đừng áp một quy tắc cho mọi API.
Thứ ba, assert giá trị, không phải văn bản. So sánh object đã phân tích trong test ổn định hơn nhiều so với so sánh đầu ra của JSON.stringify. Khi kiểm tra thất bại, hãy in đường dẫn khác biệt và giá trị trước/sau thay vì đổ toàn bộ payload vào 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
Nếu một khác biệt cần được theo dõi lâu dài, hãy commit script chuẩn hoá cùng tệp kỳ vọng. Nếu là điều tra một lần, hãy xoá script tạm sau đó để nó không âm thầm trở thành cách hiện thực thứ hai của logic nghiệp vụ.
8. Bảng chọn công cụ
| Tình huống | Lựa chọn đầu tiên | Thay thế | Lưu ý chính |
|---|---|---|---|
| Thỉnh thoảng so sánh tệp | VS Code diff | diff -u |
Định dạng trước để loại nhiễu |
| Lệch cấu hình | jq -S + diff |
delta |
Lọc trường biến động |
| Hồi quy phản hồi API | jsondiffpatch |
deepdiff |
Căn mảng theo khóa chính |
| Giải thích thay đổi cho nhóm | JSON Crack / JSON Hero | Formatter HTML | Kiểm tra dữ liệu có bị tải lên không |
| Tệp rất lớn | Trích cây con bằng jq |
Duyệt bằng jless |
Đừng nạp toàn bộ cây vào bộ nhớ |
| Kiểm tra trước commit | git diff --no-index |
pre-commit | Xử lý mã thoát một cách tường minh |
Không có công cụ JSON diff tốt nhất duy nhất; chỉ có công cụ đúng cho lớp hiện tại. Diff văn bản phát hiện thay đổi, diff cấu trúc phán xét tương đương, và trực quan hoá giải thích thay đổi. Tách ba lớp đáng tin hơn nhiều so với việc tìm một công cụ làm được mọi thứ. Với xử lý xung đột giữa các nhánh, tiếp tục với JSON và Git merge.
FAQ
jq -S có thay đổi thứ tự mảng không?
Không. -S chỉ sắp xếp khóa object và giữ nguyên thứ tự mảng. Mảng có thứ tự trong JSON, nên công cụ sẽ không biến [1,2] thành [2,1]. Nếu một mảng về mặt khái niệm là tập hợp không thứ tự, hãy sắp xếp nó một cách tường minh bằng sort_by.
1 và 1.0 có phải cùng một giá trị trong diff cấu trúc không?
Hầu hết parser biến cả hai thành cùng một số, nên diff cấu trúc thường coi chúng bằng nhau, trong khi diff văn bản đánh dấu chúng khác nhau. Hãy dùng diff cấu trúc để quyết định tương đương và diff văn bản để quyết định hai tệp có giống hệt từng byte hay không.
Có thể dùng đầu ra diff trực tiếp làm JSON Patch không?
Không trực tiếp được. Một công cụ diff báo cái gì khác nhau, còn JSON Patch mô tả một danh sách thao tác có thứ tự; định dạng và ngữ nghĩa khác nhau. Patch sinh tự động cũng thường tham chiếu chỉ số mảng, và chúng trở nên lỗi thời khi có đồng thời. Hãy thêm một thao tác test trước khi áp chúng.
Nên so sánh các tệp JSON hàng chục megabyte thế nào?
Hãy trích cây con bạn quan tâm trước, ví dụ bằng jq '.data.items', rồi sắp xếp theo khóa chính và so sánh. Đừng nạp toàn bộ cây vào bộ nhớ để chạy diff đệ quy. Tệp rất lớn nên được xử lý bằng so sánh streaming hoặc chia khối, và chi phí bộ nhớ cần được đánh giá riêng.
Công cụ diff trực tuyến có thể làm lộ dữ liệu của tôi không?
Tùy cách hiện thực. Công cụ thuần front-end tính trong trình duyệt và không tải dữ liệu lên; công cụ render phía máy chủ gửi nội dung tới backend, nơi nó có thể lọt vào log. Trước khi so sánh cấu hình production hoặc payload khách hàng, hãy xác nhận trang tính toán cục bộ, và nếu cần hãy thử một lần với mạng đã ngắt.
Làm sao quyết định giữa diff văn bản và diff cấu trúc?
Để xem nhanh hai tệp khác nhau ở đâu, hãy sắp xếp khóa bằng jq -S và chạy diff -u. Để quyết định hai tài liệu có tương đương về ngữ nghĩa không, hãy dùng diff cấu trúc như jsondiffpatch và căn mảng theo khóa chính. Diff văn bản không phụ thuộc gì và tích hợp với Git, còn diff cấu trúc không bị nhiễu bởi thụt lề, thứ tự khóa hay cách viết số.
Vì sao CI không nên coi mọi mã thoát khác 0 của diff là có khác biệt?
diff thoát với mã 1 khi tìm thấy khác biệt, nhưng tệp thiếu hoặc lỗi quyền cũng trả về khác 0. Coi mọi mã khác 0 là "nội dung đã thay đổi" sẽ báo một đường dẫn gõ sai thành thay đổi dữ liệu. Một mẫu an toàn hơn là xác minh tệp trước rồi xử lý khác biệt riêng.
Có nên diff các tệp hàng chục megabyte trong trình duyệt không?
Không. Hãy trích cây con bạn quan tâm bằng jq trước, rồi sắp xếp theo khóa chính và so sánh. Nạp toàn bộ cây vào bộ nhớ để chạy diff đệ quy tốn bộ nhớ và thời gian hơn nhiều. Tệp rất lớn nên được xử lý bằng so sánh streaming hoặc chia khối.