השוואת JSON

השוו שני מסמכי JSON וראו מה נוסף, מה הוסר ומה השתנה. סדר המפתחות, ההזחה והדחיסה אינם נחשבים, ושום דבר לא עוזב את הדפדפן.

JSON ראשון
JSON שני
תוצאה

הדביקו JSON בשני החלונות כדי להשוות ביניהם

להשוות את הנתונים, לא את הטקסט

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

{"name":"checkout","port":8080,"tags":["a","b"]}

{
  "port": 8080,
  "tags": ["a", "b"],
  "name": "checkout"
}

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

מה שלעולם אינו נחשב הבדל

ארבעה דברים בנוגע לאופן שבו מסמך כתוב מתעלמים מהם, ומתעלמים מהם ללא תנאי. אין שום פקד שמחזיר אחד מהם לפעולה, משום שכלי שהיה יכול לעשות זאת היה שני כלים עם שתי משמעויות שצריך להסביר, והקורא היה צריך לדעת מי מהשניים הפיק את התשובה שלפניו.

  • הסדר שבו כתובים איברי האובייקט. {"a": 1, "b": 2} ו-{"b": 2, "a": 1} נושאים את אותם נתונים, ולכן מנגנון סריאליזציה שפולט את המפתחות בסדר אחר בכל הרצה לעולם אינו מופיע כאן כשינוי.
  • רווחים לבנים מכל סוג — ההזחה, מעברי השורה, הרווח שאחרי הנקודתיים. מסמך מכווץ ומסמך מעוצב הנושאים את אותם נתונים הם אותו מסמך פעמיים.
  • כיצד כתוב מספר. 1e3 הוא 1000 ו-1.50 הוא 1.5, משום שלמספר ב-JSON יש ערך ואין כתיב: שני מסמכים הנבדלים רק בכך נושאים את אותם מספרים, ולא מדווח עליהם דבר.
  • כיצד מוברח תו בתוך מחרוזת. ששת התווים ‎\u0041 והאות A הם אותה מחרוזת בת תו אחד.

שני דברים שנראה כאילו מקומם ברשימה הזאת אינם בה במכוון. הסדר של איברי רשימה הוא נתון — ‎[1, 2] ו-‎[2, 1] הם מסמכים שונים — ואיבר שערכו null אינו זהה לאיבר שאיננו כלל, ולכן האחד מול השני מדווח ולא נבלע בשקט.

נוסף, הוסר, השתנה — ושינוי של טיפוס

כל שורה בתוצאה היא בדיוק מאחד משלושה סוגים, ויחד הם מכסים כל מה שההשוואה יכולה למצוא. קחו את שני המסמכים האלה:

{"host":"api.example.com","port":8080,"debug":true}

{"host":"api.example.com","port":"8080","retries":3}
  • השתנה ב-‎$['port']: האיבר נמצא בשני המסמכים וערכו אינו זהה, ולכן שני הערכים מוצגים. שורה זו מסומנת גם כשינוי של טיפוס, משום ש-8080 הוא מספר ו-"8080" הוא מחרוזת.
  • הוסר ב-‎$['debug']: האיבר נמצא במסמך הראשון ואינו נמצא בשני.
  • נוסף ב-‎$['retries']: האיבר נמצא במסמך השני ואינו נמצא בראשון.

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

היכן יושב הבדל, כתוב כנתיב

כל שורה נוקבת במקום שבו היא יושבת, והיא נוקבת בו כנתיב אל תוך המסמך ולא כמספר שורה — מספר שורה היה עובדה על הכתיב, שהוא בדיוק מה שהכלי הזה זה עתה סיים להתעלם ממנו. הנתיבים הם נתיבים מנורמלים של RFC 9535, הכתיב היחיד שהאתר הזה משתמש בו בכל מקום שבו יש לרשום מיקום בתוך JSON.

  • $ הוא המסמך כולו, ‎$['port'] הוא האיבר ששמו port שבתוכו, ו-‎$['hosts'][1] הוא האיבר במקום 1 ברשימה שמתחת ל-hosts. איבר נכתב תמיד בסוגריים מרובעים ובמירכאות, ולכן שם המכיל נקודה, רווח או סוגר משלו אינו זקוק לדבר מיוחד.
  • נתיב הוא שאילתה שאפשר להריץ. הדביקו אותו בבודק JSONPath של האתר הזה יחד עם המסמך שאליו הוא שייך, והוא יבחר בדיוק את הצומת הזה — הדרך המהירה ביותר לקרוא הבדל בסביבתו.
  • זו שאילתה כנגד המסמך שהצומת נמצא בו באמת: הנתיב של איבר שנוסף רץ על המסמך השני, והנתיב של איבר שהוסר רץ על הראשון. שום דבר כאן אינו נוקב בצומת שאיננו.

שורה שהשתנתה עשויה לשאת שני נתיבים, וזו ההתאמה שבפרק הבא מבצבצת: מרגע שרשימה הרוויחה או איבדה איבר, אותו איבר עצמו יושב במקום אחר בכל צד. כאשר שני הנתיבים זהים — וכך כמעט תמיד — מוצג רק אחד מהם.

איך מתאימים שתי רשימות זו לזו

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

{"hosts": ["alpha", "beta", "gamma"]}

{"hosts": ["alpha", "staging", "beta", "gamma"]}

זו שורה אחת: staging נוסף ב-‎$['hosts'][1]. האיברים ששמם beta ו-gamma זזו במקום אחד ואינם מוזכרים, משום שלזוז אינו להשתנות. אותה התאמה עצמה היא שמאפשרת לדווח על שדה יחיד שנערך בתוך פריט של רשימה כשינוי באותו שדה, ולא כאילו הפריט כולו הוחלף באחר.

הסדר עדיין נחשב, וההתאמה אינה מעמידה פנים אחרת: ‎[1, 2] מול ‎[2, 1] חוזר כ-1 שהוסר מהראש ו-1 שנוסף אחרי 2, וזה מה שקרה. שתי רשימות יכולות גם להיות שונות מדי מכדי שההתאמה תשתלם, ומעבר לנקודה הזאת ההשוואה נסוגה למעבר מקום אחר מקום ואומרת זאת בראש התוצאה — כך שתשובה רועשת מוסברת במקום להיות מסתורית.

הדוח והעץ

אותה השוואה מוצעת בשתי הצגות, והמעבר ביניהן אינו מחשב דבר מחדש: העץ משורטט מאותה רשימת ההבדלים ממש שהדוח מונה, ולכן השניים אינם יכולים לחלוק על מה שהשתנה או על כמה יש מזה.

  • הדוח עונה על מה השתנה: שורה אחת לכל הבדל, וכל אחת נושאת את הנתיב שלה, את הסוג שלה ואת הערך שלה בכל צד. זו התצוגה שסורקים כשרוצים את כל הרשימה לפנינו.
  • העץ עונה על היכן במסמך: שני המסמכים מותכים למתאר אחד, ופתוחים בו רק החלקים הנושאים הבדל. כל רצף של צאצאים סמוכים שאינם נושאים דבר מתקפל לשורה אחת האומרת בעד כמה היא עומדת — הזוג מהפרק שלמעלה משורטט כחמש שורות: המסמך, הרשימה שבתוכו, שורה מקופלת העומדת בעד איבר אחד, האיבר שנוסף, ושורה מקופלת העומדת בעד שניים.
  • המספר לצד כותרת התוצאה הוא כל ההבדלים שיש. כאשר הרשימה שעל המסך נקטעת בשל אורך, המספר הזה אינו זז, ופקד ברגל הרשימה מציג את השאר.
~! $['port'] 8080 -> "8080"
- $['debug'] true
+ $['retries'] 3

זה מה שפקד ההעתקה מפיק עבור שני המסמכים שבפרק השלישי: סימן לסוג, אחריו הנתיב, ואחריו הערכים עם חץ ביניהם — פלוס לאיבר שנוסף, מינוס לאיבר שהוסר, טילדה לשינוי, וטילדה עם סימן קריאה במקום שבו השתנה גם הטיפוס. הוא כתוב בסמלים ולא במילים במכוון, כדי שתוצאה שהודבקה תהיה אותו טקסט בכל אחת משפות האתר שבה קראתם, והוא אינו מקצר דבר: ערך שהמסך קטע נמצא שם בשלמותו.

מה עולה הפענוח, ומה הכלי אומר בקול

להשוות את מה ששני מסמכים מפוענחים אליו פירושו לחיות עם מה שהפענוח עולה, ופענוח JSON אינו חסר אובדן. כאן זה שוקל יותר מבכל מקום אחר באתר הזה, משום שהתשובה שכלי השוואה יכול להרשות לעצמו פחות מכול היא «אין הבדל» בין שני מסמכים שבאמת נבדלים. לכן הטקסט של כל חלון נקרא פעם שנייה כטקסט, ומה שהפענוח הפיל מדווח לצד ההשוואה במקום להיבלע.

  • שם של איבר הכתוב פעמיים באותו אובייקט. רק העותק המאוחר שורד את הפענוח, ולכן רק העותק המאוחר הושווה אי פעם ואת הקודם לא ראה איש.
  • מספר ארוך מכדי לשרוד כדיוק כפול. שני ליטרלים שונים יכולים להתפענח לאותו ערך, ומזהים הם בדיוק המקום שבו זה קורה: 12345678901234567890 ו-12345678901234567891 הם שני שלמים שונים ומספר מפוענח אחד, כך שההשוואה לבדה הייתה מכנה זהים שני מסמכים שהמזהים שלהם נבדלים.
{"user": {"id": 1}, "user": {"id": 2}}

המסמך הזה מתפענח ל-{"user": {"id": 2}}, והכלי אומר זאת מתחת לחלון שאליו הודבק, עם השורה והעמודה של המקרה הראשון וכמה יש בסך הכול. אף אחד משני האובדנים אינו עוצר את ההשוואה; שניהם ניצבים לצדה. מסמך שאינו מתפענח כלל מדווח באותו מקום, עם מיקום הבעיה בכל מקום שיש מיקום לתת, ושני החלונות נקראים בנפרד — כך ששני מסמכים שבורים מפיקים שתי שגיאות בבת אחת ולא אחת אחרי השנייה. מסמך המקונן עמוק בהרבה ממה שאפשר לשרטט ערך נדחה על הסף, וזו דחייה ולא לשונית תקועה.

למה שום דבר שאתם מדביקים אינו יוצא מהלשונית הזאת

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

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

שאלות נפוצות

האם הכלי מתעלם מסדר המפתחות?
כן, תמיד, וזו הסיבה שהכלי קיים. שני אובייקטים הנושאים את אותם איברים עם אותם ערכים זהים ולא משנה כיצד נכתבו, ולכן מנגנון סריאליזציה שפולט את המפתחות בסדר אחר בכל הרצה לעולם אינו מופיע כשינוי. ההזחה, הכיווץ, כיצד כתוב מספר וכיצד מוברח תו — מכולם מתעלמים מאותה סיבה, ואף אחד מהם אינו הגדרה שיכולתם להפוך לכיוון השני.
האם איבר שערכו null זהה לאיבר שאיננו?
לא, ואין אפשרות להתייחס לשניהם כאל אותו דבר. {"note": null} מול {} מדווח כ-‎$['note'] שהוסר. שדה שקיים וריק הוא אמירה אחרת משדה שאיש לא שלח — כל שפת סכימה וכל שפה עם טיפוסים סטטיים מבחינה בין השניים — ולכן איחוד שלהם כאן היה זורק משהו שהמסמכים באמת אומרים.
האם ‎[1, 2] ו-‎[2, 1] מדווחים כזהים?
לא. מערך JSON הוא מסודר, ולכן שתי רשימות הנושאות את אותם איברים בסדר אחר הן שני מסמכים שונים, וקריאה להם שווים הייתה הכלי הזה אומר משהו שקרי על הפורמט. מה שמתקבל הוא הסרה אחת והוספה אחת: 1 הוסר מהראש ונוסף שוב אחרי 2. אצל אובייקטים ההפך, משום שאיבריהם נקובים בשם ולא ממוקמים.
מה משמעות הנתיב בכל שורה?
זהו המקום שבו יושב ההבדל בתוך המסמך, כתוב כנתיב מנורמל של RFC 9535: $ הוא המסמך עצמו, ‎$['port'] איבר שלו, ו-‎$['hosts'][1] האיבר במקום 1 ברשימה שמתחת ל-hosts. זו גם שאילתה שאפשר להריץ — הדביקו אותה בבודק JSONPath של האתר הזה יחד עם המסמך שאליו היא שייכת, והיא תבחר בדיוק את הצומת הזה ולא שום דבר אחר.
למה שורה אחת הציגה שני נתיבים שונים?
משום שהתאמה של שתי רשימות עשויה להותיר את אותו איבר במקום אחר בכל צד. השוו בין ["alpha", "beta"] לבין ["intro", "alpha", "beta!"] ותקבלו שתי שורות: intro נוסף ב-‎$[0], ו-beta השתנה ל-beta! — שהוא ‎$[1] במסמך הראשון ו-‎$[2] בשני. כאשר שני הנתיבים זהים, וכך בדרך כלל, מוצג רק אחד מהם.
מה פירוש שינוי של טיפוס בשורה?
שהערך אינו רק שונה אלא דבר מסוג אחר: מספר שהפך למחרוזת, ערך שהפך ל-null, אובייקט שהפך לרשימה. פורט הכתוב 8080 בצד אחד ו-"8080" בצד האחר הוא המקרה השכיח ביותר, ובדרך כלל פירושו שהשתנה מנגנון סריאליזציה או לקוח ולא הנתונים. זה מסומן על השורה משום שערך שנערך וחוזה שנסחף דורשים מכם תגובות שונות.
שני המסמכים שלי נראים זהים אבל אחד מהם כותב את אותו מפתח פעמיים. מה קורה?
הפענוח שומר את העותק המאוחר ומפיל את הקודם, ולכן ההשוואה רואה איבר יחיד ובהחלט עשויה לא לדווח על שום הבדל. הכלי קורא את הטקסט הגולמי בנפרד בדיוק בשל כך, ואומר מתחת לאותו חלון ששם כתוב פעמיים, כמה פעמים בסך הכול, והיכן נמצא הראשון. אותה קריאה שנייה תופסת גם שלם ארוך מכדי לשרוד כדיוק כפול, וזו הדרך האחרת שבה שני מסמכים הנבדלים זה מזה יכולים להתפענח לשניים שאינם נבדלים.
אפשר לקבל תיקון שאפשר להחיל?
לא, וזו החלטה ולא דבר חסר. JSON Patch מוחל פעולה אחר פעולה, ולכן המקומות שבתוכו נעים תוך כדי ההרצה; להפיק כזה הוא הבטחה בנוגע לתוכנית שהייתם מריצים כנגד הנתונים שלכם עצמכם, וזו טענה גדולה בהרבה מתיאור של מה שנבדל. מה שכן אפשר לקחת במקום הוא הדוח כטקסט, מפקד ההעתקה — אותו הדבר בכל שפות האתר, ובלי לקצר בשום מקום.
האם משהו שאני מדביק נשלח לשרת?
לא. שני המסמכים מושווים בדפדפן שלכם ואף אחד מהם אינו יוצא ממנו: אין העלאה, אין בקשה ואין דבר שנשמר בין ביקורים. זה מה שהופך את הדבקת תשובה מסביבת ייצור כאן לבטוחה, וזו הסיבה שהכלי ממשיך לעבוד כשהרשת מכובה. רעננו את העמוד ושני החלונות ריקים שוב.

כלים קשורים

  • השוואת טקסטים

    כשהשינוי שאתם רוצים לראות הוא בכתיב ולא בנתונים — מעבר של הזחה, מפתח שזז ממקומו, או שני קבצים שאינם JSON כלל — ההשוואה שמראה אותו היא שורה-שורה. העמוד ההוא עושה בדיוק את זה, עם אפשרויות רווחים ורישיות ועם patch להעתקה.

  • JSON פורמט

    ולידציה ועיצוב יפה של JSON, עם מיקום שגיאות ברור.

  • SQL פורמט

    עיצוב ויפוי SQL — במגוון דיאלקטים.

  • XML פורמט

    עיצוב XML וּבדיקת תקינות מבנית — יפה או דחוס.