אותו JSON, ארבעה כלים, ארבע תוצאות: איך בוחרים מעצב
שורה תחתונה: עיצוב אינו חסר אובדן. אותו JSON עם אפס מוביל, BOM או מספר שלם בן 20 ספרות מניב תוצאות שונות ב-Python, Node, jq ו-json.tool: חלקן מדווחות על שגיאה ואחרות משנות את הערך בשקט. עשר דגימות שהורצו מקומית בארבעה מסלולים מייצרות טבלת בחירה וסקריפט בדיקה מחמיר.

מסקנה: ההבדל הקריטי ביותר בין כלי עיצוב JSON אינו מספר רווחי ההזחה, אלא שעל אותו קלט כלי אחד זורק שגיאה וכלי אחר משנה את הנתונים בשקט. לפני שבוחרים, העבירו דגימה אחת עם ערך קצה דרך הכלי — היא תגיד לכם יותר מכל רשימת תכונות.
משתמשים חושבים שעיצוב הוא רק «סידור מחדש של הטקסט». בפועל כל נתיב מפרש את הטקסט מחדש עם המפענח של עצמו. כשהמפענח סלחני, השגיאה שהייתה אמורה להיזרק הופכת לשינוי שקט של ערך — בעוד הדף ממשיך להציג את ההודעה הירוקה המרגיעה «עוצב בהצלחה».
1. התופעה: קלט אחד, ארבעה נתיבים, ארבע תוצאות
סביבת הבדיקה: Python 3.11.1, Node v22.13.1, jq 1.7. עשר מחרוזות JSON קצרות שנבנו ידנית הוזנו דרך הקלט הסטנדרטי, בלי כתיבה לדיסק ובלי שינוי תצורה. ארבעת הנתיבים: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR פירושו חריגה ועצירה, SILENT פירושו אין שגיאה אך הפלט כבר שונה מהקלט.
| דגימה | python json.loads | node JSON.parse | jq -c . | python -m json.tool |
|---|---|---|---|---|
S1 {"a":1,"b":[1,2]} |
OK | OK | OK | OK |
S2 {"a":1,} |
ERR | ERR | ERR | ERR |
S3 {'a':1} |
ERR | ERR | ERR | ERR |
S4 {"a":01} |
ERR char 6 | ERR position 6 | SILENT {"a":1} |
ERR |
S5 {"a":1 /*x*/} |
ERR char 7 | ERR | ERR | ERR |
S6 \ufeff{"a":1} |
ERR utf-8-sig | ERR position 0 | SILENT {"a":1} |
ERR |
S7 {"id":9007199254740993} |
OK exact | SILENT 9007199254740992 | OK exact | OK |
S8 {"a":1,"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
OK |
S9 {"a":1 |
ERR char 6 | ERR | ERR EOF | ERR |
S10 <html>502</html> |
ERR char 0 | ERR Unexpected token '<' | ERR | ERR |
ארבע מתוך עשר הדגימות נתנו «אין שגיאה אבל הנתונים השתנו»: S4, S6, S7, S8. כלומר מי שסומך רק על האור הירוק «עוצב בהצלחה», יש לו סיכוי של ארבעים אחוז להחזיק כבר לא את הנתונים המקוריים. הערמומיות היא שאף אחד מהמקרים האלה אינו ברשימת שגיאות התחביר — הקובץ נפתח, מועתק, מיוצא, והטעות יושבת בתוכן, לא בתחביר.
2. הסיבה: רכיב התחביר ורכיב המשמעות הם שני דברים שונים
RFC 8259 אומר זאת במפורש: סעיף 1.2 ממליץ להימנע מכמה פרקטיקות ש־ECMA-404 מתיר, לטובת מרב ההדדיות; סעיף 4 דורש שמות אובייקטים ייחודיים רק כ־SHOULD, בלי לקבוע איך לטפל במפתחות כפולים; סעיף 6 נותן למספרים רק המלצות הדדיות. ECMA-404 הולך רחוק יותר ומצהיר במפורש שהוא «אדיש לסמנטיקה של מספרים» — הוא מסדיר רק את ייצוג רצף הספרות, לא את האחסון בשפה. במילים אחרות, המפרט משאיר למממשים אזור קצה רחב.
לכן «עיצוב» מתפצל לשתי רכיבות: רכיב התחביר מחליט אם הטקסט חוקי, רכיב המשמעות מחליט לאיזה ערך הוא הופך. לראשונה יש תשובה תקנית, לשנייה לא. ש־jq קורא 01 כ־1 ומדלג על BOM בראש, זו בחירה ברכיב המשמעות; ש־Node דוחס מספר שלם בן 20 ספרות ל־double IEEE 754, זו בחירת זמן הריצה; שכל ארבעת הנתיבים שומרים את הערך האחרון ב־{"a":1,"a":2}, זו מוסכמה משותפת של מפענחים גדולים. אף אחד לא מפר את «אפשר לפענח», אבל כולם משנים נתונים.
כיצד לתקן שגיאות פענוח JSON עובד על רכיב התחביר ומתרגם את טקסט השגיאה לקואורדינטות. המאמר הזה מטפל בשלב שממש לפניו — אם הכלי לא מדווח שום שגיאה, לא תקבלו אפילו קואורדינטות.
3. ארבעה שינויים שקטים של נתונים, אחד אחרי השני
מבודדים את ארבע הדגימות השקטות, השינויים קטנים כל כך עד שקריאה חטופה לא תופסת אותם:
{"a": 01} // leading zero: jq emits {"a":1}
\ufeff{"a": 1} // BOM: jq skips the first three bytes
{"id": 9007199254740993} // 20-digit int: JSON.parse gives ...992
{"a": 1, "a": 2} // duplicate key: all four keep {"a":2}
אפס מוביל ו־BOM שייכים לסוג «הקלט כבר היה לא חוקי, אבל הכלי היה סלחני». 01 אינו חוקי ב־JSON התקני (RFC 8259 סעיף 6), וגם BOM לא אמור לפתוח טקסט JSON; Python ו־Node שניהם מדווחים שגיאה במפורש, jq בוחר להמשיך. הסלחנות הזו נוחה בניקוי לוגים, והרת אסון בבדיקת חוזה: הצד העליון חושב ששלח נתונים שגויים שצריך לתקן, בעוד הצד התחתון כבר מחזיק תוצאה «שנראית רגילה».
המספר השלם הגדול שייך לסוג «הקלט חוקי לגמרי, אבל זמן הריצה לא מכיל אותו». 9007199254740993 הוא מספר JSON חוקי, אך חורג מ־2^53-1; JSON.parse יכול רק להציג 9007199254740992. ה־int של Python ו־jq שומרים את הערך המקורי. אם ב־JSON הזה יש מספר הזמנה או Snowflake ID, ספרה אחרונה ששונתה פירושה שההתאמה החשבונית לא תסתדר.
מפתחות כפולים שייכים לסוג «המפרט שותק, המימוש בוחר». כל ארבעת הנתיבים לוקחים בשקט את הערך האחרון, בלי שום אזהרה. RFC 8259 אומר רק SHOULD, אז זה לא באג — אבל זו מחלקת הבעיות הכי קשה לאיתור כשמחברים תצורה או ממזגים טלאים.
4. יכולת איתור השגיאה: מי נותן שורה ועמודה, מי רק משפט אחד
גם בשש הדגימות שנכשלו, כמות המידע של ארבעת הנתיבים שונה מאוד. JSONDecodeError של Python נותן יחד msg, lineno, colno, pos; Node v22 מדווח position N (line 1 column M) בצירוף קטע המקור במקום השגיאה; python -m json.tool משתמש במידע של Python; jq נותן רק משפט אחד parse error: ... at line N, בלי היסט תווים.
ב־JSON ממוזער בן שורה אחת, מספר שורה ועמודה כמעט חסרי ערך — הכול בשורה 1. מה שצריך הוא היסט תווים. הסקריפט הבא מאחד את הבדיקה ל«שגיאה + היסט + הקשר»:
import json, sys
def strict_check(text: str) -> None:
try:
json.loads(text)
print("OK")
except json.JSONDecodeError as e:
print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
print("...", text[lo:hi].replace("\n", "\\n"), "...")
sys.exit(1)
strict_check(open(sys.argv[1], encoding="utf-8").read())
הוא מחמיר מ־jq . (דוחה אפס מוביל ו־BOM) ונותן e.pos. איך להמיר line 1 column 23611 למיקום בתים, ולמה היסט תווים והיסט בתים במסמכי CJK חורגים פי שלושה, מוסבר בפירוט ב־דיבוג JSON ממוזער, כאן לא נחזור.
5. התיקון: פצלו את העיצוב ל«בדיקה מחמירה» + «ייפוי»
המסקנה פשוטה: אל תיתנו לכפתור «עיצוב» אחד לשאת גם בדיקה וגם ייפוי. קודם קבעו חוקיות עם מפענח מחמיר, אחר כך בחרו כלי לפריסה. פקודה אחת מפרידה את השלב הראשון:
python3 -m json.tool input.json > /dev/null && echo "syntax OK" || echo "invalid syntax"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json
השורה הראשונה עונה רק «אפשר לפענח?», השנייה בודקת «האם המבנה שיצא הוא המצופה?». אם יש מספרים שלמים גדולים, קראו ב־Python או jq, לא ב־JSON.parse; אם המקור עלול להכיל BOM, ציינו במפורש encoding='utf-8-sig' בקריאה במקום לסמוך על דילוג אוטומטי של הכלי; אם מפתחות עלולים לחזור, סרקו בנפרד לפני הפענוח, כי אף אחד מארבעת הנתיבים לא מזהיר. כתוב ל־CI, זה בדיוק הצינור ש־עורכים, שורת פקודה ושרשראות אוטומציה מתאר, רק עם שכבה נוספת של בדיקות נאמנות נתונים.
6. איך בוחרים: טבלת השוואה של ארבעת הנתיבים
| נתיב | חוזק | סיכון | מתאים ל־ |
|---|---|---|---|
python json.loads / json.tool |
מחמיר, נותן שורה/עמודה והיסט תווים, מספרים שלמים לא מאבדים דיוק | דוחה אפס מוביל ו־BOM, קידוד צריך טיפול מפורש | בדיקת חוזים, CI, ניתוח לוגים |
node JSON.parse |
מקורי בדפדפן, השגיאה מצרפת קטע מקור | מספרים שלמים גדולים נקטמים בשקט, BOM גורם שגיאה | ניפוי פרונטאנד, כלים בדפדפן |
jq -c . |
ידידותי לצינור, התוצאה המהירה ביותר | אפס מוביל ו־BOM עוברים בשקט, אין היסט תווים | צינורות תפעול, שליפת שדה מהירה |
כלי דפדפן /parse, /editor |
תצוגת עץ, מיון מפתחות, העתקת נתיב, בלי התקנה | ההתנהגות תלויה במפענח של הדף, חייבים לבדוק פעם אחת | צפייה ידנית, איתור תקלות מזדמן |
השאלה המרכזית אינה «למי יש יותר תכונות», אלא «האם הכלי הזה משקר לגבי הנתונים שלי?». לנתונים עם מספרי הזמנה, סכומים או מזהי מכשיר, העדיפו Python או כלי שמצהיר במפורש על נאמנות נתונים; לצפייה במבנה בלבד, בלי שמירה, הדפדפן הכי נוח. /parse באתר הזה מבצע בדיקת תחביר ועיצוב בתוך הדפדפן — מתאים ל«קודם לראות איך זה נראה», אבל כשיש מספרים שלמים גדולים מומלץ לאמת שוב עם הסקריפט למעלה.
7. פרטיות היא רק אחד הגבולות
«האם הנתונים שלי יועלו?» היא השאלה הנפוצה ביותר לגבי כלים מקוונים, אבל היא נפרדת מ«האם הנתונים שלי ישתנו?»: כלי מקומי ללא חיבור לרשת עדיין יכול לקטום מספר שלם בן 20 ספרות; להפך, כלי מקוון עם שגיאות מחמירות עלול לשלוח את הטקסט המקורי ב־POST לשרת. את שתי התכונות צריך לבדוק בנפרד, אחת לא מחליפה את השנייה. שאלת הפרטיות נותנת שלוש בדיקות של שתי דקות; המאמר הזה משלים את החצי השני — בעזרת רשימת הדגימות למטה, ודאו שהכלי לא משנה ערכים בשקט.
8. רשימת בדיקה עצמית שאפשר להעתיק
שמרו את S1–S10 כ־cases.txt, דגימה אחת בשורה, הדביקו לכל כלי מועמד, והתאימו שלושה דברים: האם דגימות השגיאה באמת נכשלו? האם השגיאה ציינה מיקום קונקרטי? האם הפלט של דגימה מוצלחת זהה לקלט תו אחר תו? בדקו במיוחד את S4, S6, S7, S8 — די בשורת SILENT אחת כדי שהנתיב הזה לא יתאים לבדיקת נתונים לבדו.
הוסיפו שלושה כללי הנדסה: הכניסו בדיקה מחמירה ל־pre-commit hook, אל תגלו אי־התאמת מספרים שלמים רק בפרודקשן; כתבו קבצים כ־UTF-8 בלי BOM בכל השפות; ל־JSON מצד שלישי שמרו את גוף התגובה הגולמי, והשתמשו בתוצאה המעוצבת רק לקריאה, לא לכתיבה חזרה.
שאלות נפוצות
עיצוב הצליח, האם זה אומר שה־JSON תקין?
לא. S4, S6, S7 ו־S8 כולן «עברו» עיצוב, אבל אפס מוביל, BOM, מספר שלם בן 20 ספרות ומפתח כפול שוכתבו או נבלעו. עיצוב מוכיח רק שמפענח כלשהו קיבל את הטקסט, לא שהערכים שווים לערכים ששלחתם; בדקו שדות קריטיים ערך אחר ערך.
למה jq מקבל אפס מוביל ו־BOM ש־Python דוחה?
ל־jq יש מנתח לקסיקלי משלו, הוא סלחני למספרים כמו 01 ומדלג על BOM בראש; Python ו־Node צמודים ל־RFC 8259 בחומרה. זה לא באג גרסה, אלא הבחירה בין מפענח סלחני למחמיר. בניקוי הראשון חוסך עבודה, בבדיקת חוזה השני הכרחי.
איך מונעים ממספר שלם גדול להפוך ל־9007199254740992?
Number ב־JavaScript הוא double IEEE 754 עם גבול בטוח של 2^53-1. קריאת מזהה בן 20 ספרות דרך JSON.parse תקטום אותו בהכרח. שמרו מזהים כאלה כמחרוזת, או השתמשו ב־int של Python, jq או ספריית פענוח שתומכת BigInt; אם הפרונטאנד באמת צריך מספר, החליפו את השדה למחרוזת לפני הפענוח.
כלי מקוון או שורת פקודה מקומית — מה לקחת?
חלקו לפי רגישות ומטרה: רק לראות מבנה או למצוא שדה, הדפדפן המהיר ביותר; להיכנס לצינור ולאשר נאמנות נתונים, python -m json.tool ועוד בדיקה מחמירה. בשניהם הריצו קודם S1–S10, התאימו שגיאות ופלטים לציפייה, ורק אז החליטו איזו מחלקת נתונים להכניס.