JSON Toolkit

קריאת גבולות הדקדוק של JSON מתוך RFC 8259

שורה תחתונה: תרגום הפקות הדקדוק של RFC 8259 לשפה פשוטה: אילו תווים רשאים להופיע ישירות, אילו חייבים לעבור escape, והיכן המפרט משאיר מקום בכוונה.

מתחילכ-11 דקות קריאהפורסם 2026-10-30עודכן 2026-10-30
תמונה עבור קריאת גבולות הדקדוק של JSON מתוך RFC 8259

שורה תחתונה: כשמופיעה שגיאת תחביר ב-JSON, מצאו את התו הבלתי-חוקי הראשון וסווגו אותו כגרש בודד, פסיק עוקב, הערה או תו בקרה, במקום לקרוא את כל המסמך מחדש; בצד הקידוד, אשרו UTF-8 ללא BOM והעבירו שלמים מעבר ל-2^53 - 1 כמחרוזות.

גוף המפרט הוא רק תריסר עמודים, אבל מאחורי כל "MUST", "SHOULD" ו-"MAY" מסתתר אירוע שהשאיר פעם צוות ער כל הלילה.

1. איך שלושת הטקסטים מתייחסים זה לזה

לימוד JSON נעשה מבלבל לעיתים קרובות בגלל שלושה מספרים: RFC 4627, RFC 7159, RFC 8259, בתוספת ECMA-404. היחס ביניהם דווקא ברור למדי:

טקסט תאריך סטטוס ההבדל המרכזי
RFC 4627 2006 הוחלף הרמה העליונה חייבת להיות אובייקט או מערך
RFC 7159 2014 הוחלף הרמה העליונה הורחבה לכל ערך JSON
RFC 8259 2017.12 התקן הנוכחי UTF-8 מפורש, נוספה הדרכה לתפעול הדדי
ECMA-404 2013, מהדורה שנייה 2017.12 מפרט דקדוק מגדיר דקדוק בלבד, אינו מכסה קידוד או תעבורה

איזה מהם כדאי לקרוא? RFC 8259 לבדו מספיק. ECMA-404 שקול בחלק הדקדוק שלו; ההבדל הוא ש-RFC 8259 מגדיר גם "תעבורה כ-UTF-8" ו"סוגיות התפעול ההדדי שמיישמים צריכים לשים לב אליהן".

2. שישה טיפוסי ערכים

הפקות הדקדוק של המפרט ניתנות לקיצור לתרשים אחד:

JSON-text  = ws value ws
value      = object / array / number / string / "true" / "false" / "null"
object     = "{" [ member *( "," member ) ] "}"
member     = string ws ":" ws value
array      = "[" [ value *( "," value ) ] "]"

יש רק שש נקודות מרכזיות:

  1. אובייקטים משתמשים בסוגריים מסולסלים, האיברים מופרדים בפסיקים, ופסיק אחרי האיבר האחרון אסור;
  2. מערכים משתמשים בסוגריים מרובעים, האיברים מופרדים בפסיקים, ופסיקים עוקבים אסורים אף הם;
  3. מספרים אינם מתירים + מוביל, אפסים מובילים (01 אינו חוקי), נקודה עשרונית עירומה (1. אינו חוקי), ואינם מתירים NaN או Infinity;
  4. מחרוזות חייבות להשתמש בגרשיים כפולים; גרשיים בודדים אינם חוקיים;
  5. בוליאניים ו-null הם literal באותיות קטנות; True ו-NULL שניהם אינם חוקיים;
  6. רק תווי רווח לבן 0x20 (רווח), 0x09 (טאב), 0x0A (line feed) ו-0x0D (carriage return) רשאים להופיע בין אלמנטים מבניים.

הקטע הבא הוא JSON תקין, אף שרבים מניחים שהוא שגוי בפעם הראשונה שהם רואים אותו — מפני שהוא מחרוזת עירומה ברמה העליונה:

"just a string"

מאז RFC 7159, הרמה העליונה אכן יכולה להיות כל ערך. ב-API, עם זאת, עדיין מומלץ להשתמש רק באובייקטים או במערכים; הסיבה בסעיף 6.

3. מה באמת יכול להופיע ישירות בתוך מחרוזת

זה החלק שהכי קל להטעות בו עם "זה עובד אצלי". הכללים של המפרט למחרוזות:

  • תווים המותרים להופיע ישירות: %x20-21, %x23-5B, %x5D-10FFFF;
  • במילים אחרות, הגרש הכפול " (0x22) והקו הנטוי האחורי \ (0x5C) חייבים לעבור escape;
  • כל תווי הבקרה מתחת ל-0x20 חייבים לעבור escape, כולל line feed, טאב ו-carriage return;
  • רצפי ה-escape הזמינים הם רק \", \\, \/, \b, \f, \n, \r, \t ו-\uXXXX.
{
  "ok": "Chinese text, emoji 🎉 and full-width symbols can be written directly",
  "escaped": "A line break goes here\nand a tab\tfinishes it",
  "control": "\u0007 is the bell character and must be escaped",
  "quote": "He said: \"don't use single quotes\""
}

\/ הוא escape ישן (הוא מונע מ-</script> להיסגר מוקדם). תרחישים מודרניים כמעט ואינם זקוקים לו, אבל מפענחים חייבים לתמוך בו.

4. קידוד: UTF-8, ללא BOM

סעיף 8.1 של RFC 8259 אומר שלושה דברים:

  1. טקסט JSON המשמש לתעבורה חייב להיות מקודד ב-UTF-8;
  2. מימושים אסור שיוסיפו BOM (EF BB BF) בתחילת הטקסט;
  3. מפענחים רשאים להתעלם מה-BOM כדי להיות עמידים יותר.

שני כשלים אופייניים בעולם האמיתי:

  • Windows Notepad שומר כ"UTF-8 עם BOM", והקובץ נדחה על ידי JSON.parse עם Unexpected token;
  • קובץ JSON בסינית שנשמר כ-GBK הופך לטקסט משובש בצינור שמפענח רק כ-UTF-8, ועדיין התחביר עובר במקרה, כך שהשגיאה נדחית לשכבת העסקים.

הטיפול פשוט: הסירו את ה-BOM בזמן קריאת הקובץ, ואז פענחו במפורש כ-UTF-8; בכתיבה, ודאו שלא נוסף BOM.

/**
 * מסיר את ה-BOM של UTF-8 מתחילת מחרוזת.
 * @param text הטקסט המקורי
 * @returns הטקסט ללא BOM
 */
function stripBom(text: string): string {
  return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}

הערה על סוגי מדיה בדרך: application/json אינו מקבל יותר פרמטר charset; כתיבה שלו אינה מעלה שגיאה, אבל היא נוהג מיושן.

5. שלושה מקומות שבהם המפרט משאיר מקום בכוונה

המקומות שבהם המפרט נמנע מלקבוע דבר הם בדיוק המקור להבדלי מימושים.

1. טווח ודיוק של מספרים

RFC 8259 רק אומר "מפרט זה אינו מגביל את הטווח או הדיוק של מספרים", תוך ציון שרוב המימושים משתמשים בדיוק כפול של IEEE 754. משמעות הדבר היא ש-9223372036854775807 זהה הופך ל-9223372036854776000 ב-JavaScript, אך נשאר מדויק ב-long של Java.

מסקנה: שלמים מעבר ל-2^53 - 1, וסכומי כסף הזקוקים לדיוק של ספרות עשרוניות רבות, תמיד צריכים להיות מועברים כמחרוזות, עם pattern כתוב במפורש ב-schema.

2. מפתחות כפולים

הניסוח של המפרט הוא "השמות בתוך אובייקט SHOULD להיות ייחודיים"; הוא אינו אומר מה לעשות כשהם לא.

  • JSON.parse שומר על האחרון;
  • מפענחים מסוימים שומרים על הראשון;
  • ספריות מסוימות פשוט מעלות שגיאה.

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

3. מגבלות עומק ואורך

המפרט אינו קובע גבול לעומק הקינון, אבל לכל מימוש יש כזה. V8 זורק RangeError: Maximum call stack size exceeded בעשרות אלפי רמות קינון. שרתים חייבים להטיל מגבלות משלהם; זו דרישת אבטחה, לא דרישת מפרט.

6. בדיקת ההבנה שלכם עם דוגמאות נגד

כל שורה להלן היא JSON לא חוקי. העבירו אותן למפענח וראו אם הודעות השגיאה מועילות:

{ 'name': 'json' }        // גרשיים בודדים
{ name: "json" }          // שם המפתח ללא גרשיים
{"a": 1,}                 // פסיק עוקב
[1, 2, 3,]                // פסיק עוקב במערך
{"a": 01}                 // אפס מוביל
{"a": +1}                 // סימן פלוס מוביל
{"a": .5}                 // חסר חלק שלם
{"a": NaN}                // literal לא תקני
{"a": undefined}          // אינו ערך JSON
{"a": 1} // comment       // הערה

מקרה גבול מעניין: {"a": 1} ואחריו הערה הוא חוקי ב-JSON5, חוקי ב-JSONC, ולא חוקי ב-JSON תקני. לכן "קובץ התצורה עובד אבל ה-API דוחה אותו" הוא כמעט תמיד בעיית דיאלקט.

7. רשימת בדיקה לפרודקשן

  • [ ] כל JSON הנשלח החוצה משתמש ב-application/json בלי charset;
  • [ ] צינורות קריאה/כתיבה של קבצים מסירים את ה-BOM ומשתמשים ב-UTF-8 מפורש;
  • [ ] שלמים מעבר ל-2^53 מועברים כמחרוזות ומסומנים ב-schema;
  • [ ] מפתחות כפולים מזוהים בזמן הבנייה;
  • [ ] השרת מגביל את גודל גוף הבקשה ואת עומק הקינון;
  • [ ] ערכי הרמה העליונה של APIs ציבוריים הם אובייקטים או מערכים, לעולם לא סקלרים עירומים.

שאלות נפוצות

איך יודעים במהירות אם קטע טקסט הוא JSON תקין?

העבירו את הטקסט למפענח או לכלי פענוח מקוון, קראו את המיקום של התו הבלתי-חוקי הראשון, ואז בדקו אותו מול ששת טיפוסי הערכים וכללי הרווח הלבן של RFC 8259. הרמה העליונה יכולה להיות כל ערך JSON, אבל בין אלמנטים מבניים מותרים רק רווח, טאב, line feed ו-carriage return. גרש בודד, מפתח ללא גרשיים, פסיק עוקב או הערה משמעם שזה אינו JSON תקני.

למה JSON חייב להשתמש ב-UTF-8 וללא BOM?

סעיף 8.1 של RFC 8259 דורש שטקסט JSON המשמש לתעבורה יהיה מקודד ב-UTF-8, אומר שמימושים אסור שיוסיפו BOM בהתחלה, ומאפשר למפענחים להתעלם מה-BOM למען עמידות. קובץ עם BOM נדחה על ידי JSON.parse עם Unexpected token, למרות ששום דבר לא נראה שגוי לעין. לכן הסירו את ה-BOM בזמן הקריאה ופענחו במפורש כ-UTF-8.

כדאי להשתמש ב-JSON5 או ב-JSONC כדי לעקוף את מגבלות הדקדוק האלה?

כן, אבל רק לקריאה מקומית כמו קובצי תצורה; נתונים המוחלפים בין מערכות צריכים להישאר JSON תקני. דוגמאות הנגד מראות ש-{"a": 1} ואחריו הערה הוא חוקי ב-JSON5 וב-JSONC אבל לא חוקי ב-JSON תקני, ולכן "קובץ התצורה עובד אבל ה-API דוחה אותו" הוא כמעט תמיד בעיית דיאלקט. בהמרה, הסירו קודם את ההערות, הגרשיים הבודדים והפסיקים העוקבים ומסרו את התוצאה למפענח תקני.