בחירת כלי JSON diff: diff טקסטואלי, diff מבני והמחשה
שורה תחתונה: diff טקסטואלי מוצא היכן הקבצים שונים; diff מבני קובע אם התוכן שקול. בחירת השכבה הנכונה מונעת מכך שסדר מפתחות ייחשב בטעות לתקלה.

שורה תחתונה: diff טקסטואלי מגלה את השינוי (מיינו מפתחות עם jq -S קודם), diff מבני קובע שקילות (יישרו מערכים לפי מפתח ראשי), והמחשה רק מסבירה או סוקרת; שמרו על שלוש השכבות נפרדות ב-CI.
שני מסמכי JSON שנראים שונים אינם אומרים שהתוכן שלהם שונה. בחרו את שכבת ההשוואה הנכונה לפני שאתם בוחרים בכלי.
1. שלושה סוגים של "diff" אינם אותו דבר
צוותים רבים מתייחסים ל"השוואת JSON" כפעולה אחת, ואז נתקלים באותן בעיות בתרחישים שונים. כדאי להפריד שלוש שכבות:
| שכבה | מה מושווה | מה היא חושפת | שימוש טיפוסי |
|---|---|---|---|
| diff טקסטואלי | בתים ושורות גולמיים | הזחה, סדר מפתחות, רווחים, שורות חדשות | סטייה בהגדרות, סקירת קוד |
| diff מבני | אובייקטים ומערכים מפורסרים | מפתחות שנוספו או הוסרו, ערכים שהשתנו, שינויי איברים במערך | רגרסיה ב-API, מיגרציית נתונים |
| diff חזותי | איך ההבדלים מוצגים | לאיזו רמה שייך השינוי, כמה גדול רדיוס ההשפעה שלו | סקירה אנושית, דיון בצוות |
diff טקסטואלי חסר תלויות, מהיר ומשתלב באופן טבעי ב-Git. החולשה שלו היא שהוא אינו יודע דבר על סמנטיקת JSON. שני מסמכים לחלוטין שקולים יידלקו באדום ובירוק אם סדר המפתחות או ההזחה שלהם שונים. diff מבני הוא ההיפך: הוא קודם מפרסר ואז משווה, ולכן הוא מזהה שערך רק עבר מקום. אבל הוא צריך אסטרטגיית יישור מערכים נכונה; אחרת הוספה בודדת נקראת בטעות ככתיבה מחדש מלאה.
המחשה אינה אלגוריתם שלישי. היא מציגה את הפלט של שתי השכבות האחרות כדי שאדם יוכל לאשר במהירות אם ההבדל צפוי. היא טובה לשיקול אנושי וגרועה כתחליף לבדיקות אוטומטיות. ערבוב השכבות יחד מייצר בדרך כלל צינור CI שתמיד אדום וצוות שלמד להתעלם ממנו.
2. diff טקסטואלי: נקו את סדר המפתחות עם jq -S קודם
למבט מהיר בשני קבצים, השילוב הזול ביותר הוא jq -S בתוספת diff -u. הדגל -S ממיין מפתחות אובייקט רקורסיבית, ומנרמל מסמכים שתוכנם זהה אך סדר המפתחות שלהם שונה:
jq -S . a.json > a.sorted.json
jq -S . b.json > b.sorted.json
diff -u a.sorted.json b.sorted.json
זה מסיר את החיוב הכוזב הנפוץ ביותר. RFC 8259 מגדיר אובייקטים כאוספים לא מסודרים, כך ש-{"a":1,"b":2} ו-{"b":2,"a":1} הם אותו ערך. diff טקסטואלי, לעומת זאת, מסמן כל שורה כשונתה. מיון מראש מקרב את הבלוקים האדומים הרבה יותר לבעיות אמיתיות.
שימו לב ש-jq -S ממיין מפתחות בלבד; הוא אינו ממיין מערכים. סדר מערך הוא משמעותי ב-JSON, ו-[1,2] אינו אותו ערך כמו [2,1], ולכן הכלי לא יקבל את ההחלטה הזו בשבילכם. אם מערך הוא מבחינה רעיונית קבוצה לא מסודרת, מיינו אותו לפי מפתח ראשי בעצמכם לפני ההשוואה.
יש עוד פרט שכדאי לזכור: diff יוצא עם קוד 1 כשהוא מוצא הבדלים, אבל קובץ חסר או שגיאת הרשאה גם מייצרים יציאה שאינה אפס. ב-CI אל תתייחסו לכל קוד שאינו אפס כ"התוכן השתנה", אחרת נתיב שהוקלד בטעות ידווח כשינוי נתונים. דפוס בטוח יותר מאמת את הקבצים קודם ואז מטפל בהבדל:
if ! diff -u a.sorted.json b.sorted.json; then
echo "JSON content differs" >&2
exit 1
fi
delta הופך את 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. diff מבני: jsondiffpatch מבין סמנטיקה
כשהשאלה היא אם שתי פיסות נתונים שקולות מבחינה סמנטית, השתמשו ב-diff מבני. האפשרות הנפוצה ביותר באקוסיסטם של JavaScript היא jsondiffpatch, שמחזיר אובייקט delta המתאר שינויים ולא שורות טקסט עם סימוני + ו--:
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 מבדיל בין מפתחות שנוספו, מפתחות שהוסרו, ערכים שהוחלפו ואיברי מערך שהועברו. זה אומר שאפשר לצעוד ישירות ב-delta בתוך בדיקות: undefined כשאין הבדל, או תיאור נתיב-אחר-נתיב כשיש. בניגוד ל-diff טקסטואלי, הוא אינו מוטרד מהזחה, מסדר מפתחות או מצורת כתיבה מספרית כמו 1e3 לעומת 1000.
חשוב לא פחות: פלט diff מבני יכול להזין זרימת עבודה של patching. jsondiffpatch מספק patch ו-reverse, כך שאפשר ליישם delta על האובייקט המקורי ולבטל אותו אחר כך. הסתייגות אחת: delta שנוצר אוטומטית אינו באותו פורמט כמו JSON Patch בכתב יד, והמרה ביניהם דורשת עבודה. לגישה רחבה יותר של השוואה, ראו JSON diff והשוואה.
4. איך לחסל הבדלים כוזבים שנגרמים מסדר מפתחות
סדר מפתחות אינו רק מעצבן; הוא יכול לייצר מסקנות שגויות. פעלו בשלושה שלבים בחקירה:
- לפני ההשוואה, נרמלו את סדר המפתחות רקורסיבית עם
jq -Sאו כלי שקול. - הסירו שדות שמשתנים בכל ריצה, כמו
timestamp,requestIdו-traceId. - ודאו שאין מפתחות כפולים. מפתחות כפולים נפתרים ב"אחרון מנצח" בזמן הפרסור, כך ש-diff טקסטואלי רואה אותם ו-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
הזרימה הזו מתאימה היטב לסטייה בהגדרות. הריצו את staging ואת production דרך אותו נרמול, וכל מה שנשאר הוא ההבדל שבאמת דורש הסבר. שמרו את חוקי הסינון בסקריפט מנוהל גרסאות במקום להקליד אותם מחדש מהזיכרון. לצד העריכה והסקריפטים של שרשרת הכלים, ראו עורכים, שורת פקודה ואוטומציה.
5. מערכים: ליישר לפי אינדקס או לפי id?
מערכים הם החלק השגוי ביותר בהשוואת JSON. ביישור ברירת מחדל לפי אינדקס, הוספת רשומה אחת בהתחלה מזיזה כל איבר מאוחר יותר, והכלי מסמן את כולם כשונו. הכלי לא שבור; אסטרטגיית היישור שגויה.
| יישור | עובד כש | חוזק | סיכון |
|---|---|---|---|
| לפי אינדקס | לסדר המערך יש משמעות עסקית | פשוט, אין צורך בשדות נוספים | הוספה אחת גוררת חיוביים כוזבים משורשרים |
| לפי מפתח ראשי | האיברים הם קבוצה עם id ייחודי | מזהה הוספה, מחיקה והעברות | דורש מפתח יציב וייחודי |
| תת-רצף משותף ארוך ביותר | הסדר חשוב אבל הוספות מותרות | עמיד יותר מיישור אינדקס טהור | קשה יותר להסבר, עלות גבוהה יותר |
jsondiffpatch משתמש ב-objectHash כדי להכריז על מפתח ראשי, ומאיין את איברי המערך לפני ההשוואה:
const instance = create({
objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});
עם jq רגיל, אפשר למיין קודם ואז להשתמש ב-diff טקסטואלי:
jq -S 'sort_by(.id)' items.json > items.sorted.json
הפשרה ביישור לפי id היא ששינויים במיקום כבר אינם נחשבים להבדלים. אם העסק צריך לדעת שרשומה עברה ממיקום 2 למיקום 5, השוו גם שדה סדר מפורש; אל תסתמכו על תוכן הקבוצה בלבד.
6. diff חזותי: השכבה שנבנתה לאנשים
פלט diff מבני מיועד למכונות, לא לפגישות. כשצריך להסביר שינוי למישהו אחר, המחשה אפקטיבית יותר:
- עורך ה-diff של VS Code טוב לסקירה קובץ-אחר-קובץ, עם חלוניות זו-לצד-זו והדגשות מוטבעות.
- מציגי עצים כמו JSON Crack ו-JSON Hero מראים לאיזו רמה שייך השינוי.
jsondiffpatchכולל מעצב HTML שמציג delta כתצוגה עשירה הניתנת לקיפול.
הערך של המחשה הוא ההקשר. diff טקסטואלי אומר ששורה 88 השתנתה; תצוגת עץ אומרת ששורה 88 שייכת ל-data.items[3].price. אבל המחשה בדרך כלל טוענת את כל המסמך לדפדפן או לאפליקציית שולחן עבודה. לפני פתיחת מטען production, ודאו אם הכלי מחשב מקומית או מעלה לשרת.
7. שלושה נוהגים לשילוב diff ב-CI
ראשית, נרמלו לפני ההשוואה. קבעו הזחה, מיינו מפתחות רקורסיבית וסננו שדות תנודתיים לפני שה-diff רץ. אחרת CI ייכשל שוב ושוב על רעש.
שנית, בחרו יישור לפי סמנטיקה. אם מערך הוא קבוצה, יישרו לפי מפתח ראשי. אם הוא רצף, שמרו על יישור לפי אינדקס. אל תחילו חוק אחד על כל API.
שלישית, אמתו ערכים, לא טקסט. השוואת אובייקטים מפורסרים בבדיקות יציבה הרבה יותר מהשוואת הפלט של JSON.stringify. כשבדיקה נכשלת, הדפיסו את נתיב ההבדל ואת הערכים לפני ואחרי במקום להשליך את כל המטען ללוג.
# 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 לסקריפט הנרמול ולקובץ הצפוי יחד. אם מדובר בחקירה חד-פעמית, מחקו את הסקריפט הזמני אחר כך כדי שלא יהפוך בשקט ליישום שני של לוגיקה עסקית.
8. טבלת בחירת כלים
| תרחיש | בחירה ראשונה | חלופה | תזכורת מרכזית |
|---|---|---|---|
| השוואת קבצים מזדמנת | diff של VS Code | diff -u |
עצבו קודם כדי להסיר רעש |
| סטייה בהגדרות | jq -S + diff |
delta |
סננו שדות תנודתיים |
| רגרסיה בתגובת API | jsondiffpatch |
deepdiff |
יישרו מערכים לפי מפתח ראשי |
| הסבר שינוי לצוות | JSON Crack / JSON Hero | מעצב HTML | בדקו אם הנתונים מועלים |
| קבצים גדולים מאוד | חילוץ תת-עץ עם jq |
עיון עם jless |
אל תטענו את כל העץ לזיכרון |
| בדיקה לפני commit | git diff --no-index |
pre-commit | טפלו בקודי יציאה במפורש |
אין כלי JSON diff יחיד שהוא הטוב ביותר; יש רק הכלי הנכון לשכבה הנוכחית. diff טקסטואלי מגלה שינוי, diff מבני שופט שקילות, והמחשה מסבירה שינוי. שמירה על שלוש השכבות נפרדות אמינה הרבה יותר מחיפוש אחר כלי אחד שעושה הכול. לטיפול בקונפליקטים בין ענפים, המשיכו ל-JSON ומיזוג Git.
FAQ
האם jq -S משנה גם את סדר המערך?
לא. -S ממיין מפתחות אובייקט בלבד ומשאיר מערכים בסדר המקורי שלהם. מערכים מסודרים ב-JSON, ולכן הכלי לא יהפוך [1,2] ל-[2,1]. אם מערך הוא מבחינה רעיונית קבוצה לא מסודרת, מיינו אותו במפורש עם sort_by.
האם 1 ו-1.0 הם אותו ערך ב-diff מבני?
רוב המפרסרים הופכים את שניהם לאותו מספר, ולכן diff מבני בדרך כלל מחשיב אותם כשווים, בעוד diff טקסטואלי מסמן אותם כשונים. השתמשו ב-diff מבני כדי להחליט על שקילות וב-diff טקסטואלי כדי להחליט אם קבצים זהים בית-אחר-בית.
אפשר להשתמש בפלט diff ישירות כ-JSON Patch?
לא ישירות. כלי diff מדווח מה שונה, בעוד JSON Patch מתאר רשימה מסודרת של פעולות; הפורמטים והסמנטיקה שונים. patch-ים שנוצרים אוטומטית גם נוטים להפנות לאינדקסים של מערך, שמתיישנים תחת מקביליות. הוסיפו פעולת test לפני יישום.
איך להשוות קובצי JSON בגודל של עשרות מגה-בייט?
חלצו קודם את תת-העץ שמעניין אתכם, למשל עם jq '.data.items', אחר כך מיינו לפי מפתח ראשי והשוו. אל תטענו את כל העץ לזיכרון עבור diff רקורסיבי. קבצים גדולים מאוד עדיף לטפל בזרימה או בהשוואה בחלקים, ועלות הזיכרון מחייבת הערכה נפרדת.
האם כלי diff מקוונים יכולים להדליף את הנתונים שלי?
זה תלוי ביישום. כלים חזיתיים טהורים מחשבים בדפדפן ואינם מעלים נתונים; כלים שמבוססים על צד שרת שולחים תוכן ל-backend, שם הוא עלול להיכנס ללוגים. לפני השוואת הגדרות production או מטעני לקוחות, ודאו שהדף מחשב מקומית, ובמידת הצורך בדקו פעם אחת עם הרשת מנותקת.
איך מחליטים בין diff טקסטואלי ל-diff מבני?
למבט מהיר היכן שני קבצים שונים, מיינו מפתחות עם jq -S והריצו diff -u. כדי להחליט אם שני מסמכים שקולים סמנטית, השתמשו ב-diff מבני כמו jsondiffpatch ויישרו מערכים לפי מפתח ראשי. diff טקסטואלי חסר תלויות ומשתלב ב-Git, בעוד diff מבני אינו מוטרד מהזחה, מסדר מפתחות או מצורת כתיבה מספרית.
למה CI לא צריך להתייחס לכל קוד יציאה לא-אפס של diff כהבדל?
diff יוצא עם 1 כשהוא מוצא הבדלים, אבל קובץ חסר או שגיאת הרשאה גם מחזירים ערך לא-אפס. התייחסות לכל קוד לא-אפס כ"התוכן השתנה" מדווחת על נתיב שהוקלד בטעות כשינוי נתונים. דפוס בטוח יותר מאמת את הקבצים קודם ואז מטפל בנפרד בהבדל.
כדאי להריץ diff על קבצים של עשרות מגה-בייט בדפדפן?
לא. חלצו קודם את תת-העץ שמעניין אתכם עם jq, אחר כך מיינו לפי מפתח ראשי והשוו. טעינת כל העץ לזיכרון עבור diff רקורסיבי עולה הרבה יותר זיכרון וזמן. קבצים גדולים מאוד עדיף לטפל בזרימה או בהשוואה בחלקים.