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

שורה תחתונה: נעלו בחוזה את מעטפת התשובה, פורמט השגיאה, ייצוג המזהים והזמן והסמנטיקה של null מול היעדר לפני שאתם מתווכחים על עימוד ופרטי שמות; תמיד עטפו את הרמה העליונה במעטפת data כדי שעימוד ומזהי מעקב יוכלו להיווסף אחר כך בלי לשבור קוראים קיימים.
אין תשובות סטנדרטיות בתכנון API, אבל יש כמה החלטות "קשות לקחת בחזרה אחרי שנבחרו" שמוטב לחשוב עליהן לפני שכותבים את שורת הקוד הראשונה.
1. לעטוף את התשובה או לא
שלושה סגנונות:
// סגנון A: תשובה חשופה
{ "id": 1, "name": "张三" }
// סגנון B: מעטפת data
{ "data": { "id": 1, "name": "张三" } }
// סגנון C: data + מטא-נתונים
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
| סגנון | יתרונות | חסרונות |
|---|---|---|
| תשובה חשופה | הפשוט ביותר, רמה אחת פחות לפרונט | אי אפשר לצרף מטא-נתונים; מערך ברמה העליונה נושא סיכוני חטיפת JSON ואבולוציה |
| מעטפת data | קל להוסיף meta / errors / links |
רמת קינון נוספת, הפרונט צריך לכתוב res.data.data |
| data + מטא-נתונים | observability הטוב ביותר | דורש הסכמה קפדנית על אילו שדות שייכים ל-meta |
המלצה: השתמשו במעטפת data אחידה גם לרשימות וגם לפרטים, ולעולם אל תהפכו את הרמה העליונה למערך. כך, הוספת עימוד או מזהי מעקב בהמשך לעולם לא שוברת את החוזה.
2. פורמט שגיאה: העתיקו את RFC 9457
המצאת מבנה שגיאה משלכם היא מקור עצום לעלות תיאום. RFC 9457 (שמחליף את RFC 7807) מגדיר application/problem+json:
{
"type": "https://bbdrug.com/problems/insufficient-balance",
"title": "余额不足",
"status": 422,
"detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
"instance": "/accounts/A1001/payments/p-889",
"balance": "12.00",
"required": "30.00"
}
| שדה | משמעות | נדרש |
|---|---|---|
type |
URI יציב לסוג הבעיה | מומלץ |
title |
סיכום קצר קריא לבני אדם | מומלץ |
status |
קוד סטטוס HTTP | מומלץ |
detail |
הסבר ספציפי למה שקרה הפעם | אופציונלי |
instance |
מופע המשאב שבו הייתה הבעיה | אופציונלי |
| שדות הרחבה | שדות עסקיים שרירותיים | אופציונלי |
שדות הרחבה (כמו balance / required שלמעלה) מותרים במפרט, והפרונט יכול להשתמש בהם להודעות מדויקות במקום לפרסר את טקסט ה-detail.
3. עימוד: cursor עדיף על offset
// עימוד offset
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }
// עימוד cursor
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
| ממד | עימוד offset | עימוד cursor |
|---|---|---|
| קושי מימוש | נמוך (LIMIT/OFFSET) |
בינוני (דורש מפתח מיון יציב) |
| ביצועים בדפים עמוקים | גרועים (ככל שה-offset גדל, זה נהיה איטי יותר) | טובים |
| כשהנתונים משתנים | כפילויות או פריטים חסרים | יציב |
| קפיצה לדף | אפשרית | לא אפשרית |
| דורש ספירה כוללת | דורש ספירה נוספת | לא נדרש |
המלצה: השתמשו בעימוד offset לטבלאות ניהול (הן צריכות קפיצות דף וסכומים), ובעימוד cursor לפידים אינסופיים שמיועדים למשתמשים.
4. מזהים וזמן
4.1 תמיד השתמשו במחרוזות למזהים
הנימוק כוסה במאמר על טיפוסים: בצד של JavaScript, ערכים מעל 2^53 מאבדים דיוק בשקט.
גם אם המזהה הנוכחי הוא מספר שלם קטן שעולה אוטומטית, שימוש במחרוזות משאיר מקום למעבר למזהי snowflake בהמשך.
{ "id": "1000000000000000001" }
4.2 השתמשו במחרוזות RFC 3339 לזמן
{
"createdAt": "2026-10-30T10:41:45+08:00",
"updatedAt": "2026-10-30T02:41:45Z"
}
- אל תשתמשו במספרי חותמת זמן במילישניות: קשים לקריאה, והיחידה (שניות/מילישניות) נטעית לעתים קרובות;
- ציינו את אזור הזמן במפורש: או עם
Zאו עם offset; לעולם אל תשתמשו ב"שעה מקומית בלי אזור זמן"; - תנו שמות אחידים לשדות כמו
xxxAtכדי שיהיה ברור במבט חטוף שזה רגע בזמן.
5. null והיעדר
חייבים לבחור אחת ולכתוב אותה בתיעוד:
| גישה | ביטוי "אין ערך" | ביטוי "אין לשנות" |
|---|---|---|
| גישה A | ערך השדה הוא null |
השמיטו את השדה בבקשה |
| גישה B | השדה לא קיים | השמיטו את השדה בבקשה |
הבעיה בגישה A היא ש"הושמט" ו"נשלח כ-null" משמעותם שונה ב-PATCH, וקל לפרש לא נכון;
הבעיה בגישה B היא ש-JSON.stringify של הפרונט משמיט שדות שערכם undefined, וזה במקרה מתאים לסמנטיקה.
המלצה מעשית:
- השתמשו בגישה A לגופי תשובה (
nullפירושו בבירור אין ערך, והפרונט לא צריך להבחין ביןundefined); - השתמשו בגישה B לגופי בקשה (במיוחד
PATCH), בשילוב Merge Patch או JSON Patch כדי להפוך את הסמנטיקה למפורשת.
6. שמות שדות ומבנה
| החלטה | בחירות נפוצות | הסתייגות |
|---|---|---|
| סגנון שמות | camelCase (אקוסיסטם JS) / snake_case (Python, מסדי נתונים) |
אחיד בפרויקט, אל תערבבו |
| ערכי enum | אותיות קטנות עם קו תחתון, "pending_review" |
אל תשתמשו במספרים; קשים לקריאה |
| שדות בוליאניים | isActive / enabled |
אל תשתמשו ב-status: 0/1 |
| שדות מערך | שמות עצם ברבים, items |
החזירו [] למערכים ריקים, לעולם לא null |
| עומק קינון | לא יותר מ-3 רמות | עמוק מזה, פצלו אותו או הציעו פרמטר expand |
החזרת [] במקום null עבור מערכים ריקים חשובה מאוד: data.items.map(...) בפרונט לא יקרוס בגלל null.
7. אסטרטגיית אבולוציה
| שינוי | שובר תאימות | איך לטפל |
|---|---|---|
| הוספת שדה אופציונלי | לא | שחררו ישירות |
| הוספת שדה נדרש | כן | הוסיפו ערך ברירת מחדל, או שחררו לפי גרסה |
| הסרת שדה | כן | סמנו אותו כ-deprecated קודם, עקבו אחרי הקוראים, ואז הסירו |
| שינוי טיפוס שדה | כן | הוסיפו שם שדה חדש וכתבו כפול בזמן המעבר |
| שינוי קבוצת ערכי enum | תלוי | ללקוח חייב להיות fallback לערכים לא מוכרים |
| שינוי סמנטיקת שדה | כן | התייחסו אליו כשדה חדש |
שלושה כללי משמעת:
- לקוחות חייבים להיות סובלניים לשדות לא מוכרים (אל תסגרו את עצמכם עם
additionalProperties: false); - ללקוחות חייב להיות ענף fallback לערכי enum לא מוכרים;
- שמרו שדות deprecated לפחות גרסה major אחת, וסמנו אותם עם
Deprecationבכותרות התשובה או בתיעוד.
8. דוגמה מלאה
{
"data": [
{
"id": "1001",
"name": "JSON 权威指南",
"price": "89.50",
"tags": ["json", "spec"],
"publishedAt": "2026-09-01T00:00:00+08:00",
"author": null
}
],
"page": { "nextCursor": null, "hasMore": false },
"meta": { "requestId": "req-7f3a", "tookMs": 12 }
}
התשובה הזו משקפת את כל המוסכמות שלמעלה: מעטפת data, מזהים כמחרוזות, סכומים כמחרוזות, חותמות זמן עם אזור זמן,
null שפירושו חסר, מערך ריק, עימוד cursor, ו-meta למידע observability.
9. צ'קליסט
- [ ] הרמה העליונה אינה מערך; השתמשו במעטפת
dataאחידה; - [ ] תשובות שגיאה עוקבות אחרי RFC 9457 וכוללות
typeיציב; - [ ] צורת העימוד מתאימה לתרחיש העסקי;
- [ ] מזהים הם מחרוזות, והזמן משתמש ב-RFC 3339;
- [ ] הסמנטיקה של
nullוהיעדר כתובה בתיעוד; - [ ] סגנון השמות אחיד בפרויקט, ו-enums משתמשים במחרוזות באותיות קטנות;
- [ ] ללקוח יש fallback לשדות לא מוכרים ולערכי enum לא מוכרים;
- [ ] שינויים שוברים עוברים דרך גרסאות או מעבר בכתיבה כפולה.
FAQ
איך אתכנן JSON API שניתן לאבולוציה?
עטפו רשימות ופרטים באופן אחיד במעטפת data ולעולם אל תהפכו את הרמה העליונה למערך, כדי שאפשר יהיה להוסיף עימוד ומזהי מעקב בלי לשבור את החוזה. השתמשו במזהים כמחרוזות ובחותמות זמן RFC 3339, וכתבו בתיעוד את הסמנטיקה של null מול היעדר. השאירו שדות חדשים כאופציונליים ועצמאיים סמנטית, ותנו ללקוחות fallback לשדות ולאומי enum לא מוכרים.
למה לא להמציא פורמט שגיאה מותאם?
המצאת מבנה שגיאה משלכם היא מקור עצום לעלות תיאום. העתיקו את application/problem+json של RFC 9457, זהו בעיות עם type יציב, והתירו שדות הרחבה כמו balance ו-required, כדי שהפרונט יוכל לבנות הודעות מדויקות בלי לפרסר את טקסט ה-detail.
האם כל endpoint צריך להשתמש בעימוד cursor?
לא. טבלאות ניהול צריכות קפיצות דף וסכומים, ולכן עימוד offset מתאים יותר; פידים אינסופיים שמיועדים למשתמשים צריכים להשתמש בעימוד cursor, שנשאר יציב בדפים עמוקים ונמנע מכפילויות או פערים כשהנתונים משתנים. בחרו לפי האם התרחיש צריך קפיצות דף וסכומים.