Menu

JSON ב-Golang: Marshal, Unmarshal ו-struct tags

איך מקודדים ומפענחים JSON ב-Go עם encoding/json: Marshal ו-Unmarshal, struct tags כמו omitempty ו-omitzero, הדפסה מעוצבת, פענוח לתוך map[string]any, דחיית שדות לא מוכרים ו-streaming עם Decoder.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

Marshal ו-Unmarshal

json.Marshal הופכת ערך Go לבייטים של JSON. json.Unmarshal ממלאת ערך Go מבייטים של JSON. struct tags קובעים את שמות השדות ב-JSON.

Email חסר בפלט הראשון בגלל omitempty ומחרוזת ריקה. Unmarshal צריכה pointer (&back); העברה של הערך עצמו מחזירה InvalidUnmarshalError.

Struct tags

התחביר של tag הוא json:"name,option,option".

Tagהשפעה
json:"user_id"משתמש ב-user_id כמפתח
json:"email,omitempty"משמיט כשהערך הוא false, 0, "", nil, או slice או map ריקים
json:",omitzero" (Go 1.24)משמיט כשהערך הוא ערך האפס שלו, או כש-IsZero() שלו מחזירה true
json:"-"אף פעם לא מקודד או מפענח את השדה הזה
json:"-,"משתמש במפתח המילולי -
json:"count,string"מקודד מספר או bool כמחרוזת JSON ("42")
בלי tagהמפתח הוא שם השדה ב-Go, UserID

הסיסמה אף פעם לא יוצאת מה-struct, היתרה מופיעה במירכאות, ו-deleted_at נעלם בזמן ש-created_at מדפיס את זמן האפס 0001-01-01T00:00:00Z. ההבדל הזה הוא הסיבה שנוסף omitzero: omitempty אף פעם לא עבד על structs, וזו הפתעה ותיקה עם time.Time. ב-Go 1.23 ומטה, השתמשו ב-*time.Time עם omitempty כדי לקבל את אותה תוצאה.

MarshalIndent(v, prefix, indent) מייצרת פלט קריא. השתמשו בה לקובצי הגדרות ולדיבאג; APIs בדרך כלל שולחים JSON דחוס.

רק שדות מיוצאים

encoding/json משתמשת ב-reflection ויכולה לראות רק שדות מיוצאים. זה באג ה-JSON הנפוץ ביותר ב-Go:

type point struct {
	x, y int // lowercase: json.Marshal(point{1, 2}) gives {}
}

בלי שגיאה, בלי אזהרה, רק {} ביציאה ושדות שנשארים באפס בכניסה. כתבו את השדות באות גדולה והוסיפו tags למפתחות באותיות קטנות.

איך הפענוח מתאים שדות

כשעושים unmarshal לתוך struct:

  • מפתחות מותאמים לשם ב-tag, או לשם השדה, בלי הבחנה בין אותיות גדולות לקטנות. {"NAME": "x"} ממלא שדה עם tag json:"name".
  • מפתחות בלי שדה מתאים מתעלמים מהם בשקט.
  • שדות בלי מפתח מתאים שומרים על הערך הנוכחי שלהם. Unmarshal לא מאפסת אותם, כך שפענוח לתוך struct שכבר יש בו נתונים ממזג לתוכו.
  • אי התאמה בטיפוס (מחרוזת במקום שבו ב-struct יש int) מחזירה *json.UnmarshalTypeError, אבל שאר השדות עדיין מתמלאים.

כדי לדחות מפתחות לא צפויים, למשל ב-API קפדני או בקובץ הגדרות עם שגיאות כתיב, השתמשו ב-Decoder עם DisallowUnknownFields:

מבנה לא ידוע: map[string]any

כשלא יודעים את המבנה מראש, פענחו לתוך map[string]any או any. המיפוי קבוע:

JSONGo
אובייקטmap[string]any
מערך[]any
מחרוזתstring
מספרfloat64
true / falsebool
nullnil

שתי מלכודות נראות בפלט. כל מספר הוא float64, ולכן m["stock"].(int) הייתה נכשלת. ומספרים שלמים מעל 2^53 מאבדים דיוק כ-float64: ה-id 9007199254740993 חוזר כ-...992. UseNumber מונעת את שתיהן.

אם אתם מכירים חלק מהמבנה, פענחו את החלק הזה לתוך struct והשתמשו ב-json.RawMessage לשאר. הוא מחזיק את הבייטים הגולמיים של שדה, כך שאפשר לפענח אותו מאוחר יותר, כשיודעים את הטיפוס שלו.

Slices, maps, pointers ו-nil

ערך GoJSON
slice שהוא nil (var s []int)null
slice ריק ([]int{})[]
map שהוא nilnull
pointer שהוא nilnull
[]byteמחרוזת base64
map[string]Tאובייקט, עם מפתחות ממוינים
map[int]Tאובייקט עם המפתחות השלמים כמחרוזות

ההבדל בין slice שהוא nil ל-slice ריק חשוב ללקוחות API שמצפים למערך. אתחלו עם []T{} או make([]T, 0) כשהשדה חייב להיות [].

השתמשו בשדה pointer (*int, *bool) כשצריך להבחין בין "חסר" ל"אפס" בקלט. אחרי unmarshal, pointer שהוא nil אומר שהמפתח היה חסר או null; pointer ל-0 אומר שהלקוח שלח 0.

Streams: Encoder ו-Decoder

json.Marshal ו-Unmarshal עובדות על slices שלמים של בייטים. ל-io.Reader או ל-io.Writer (body של HTTP, קובץ, stdin), השתמשו ב-json.NewDecoder וב-json.NewEncoder. Decoder יכול גם לקרוא רצף של ערכי JSON אחד אחרי השני:

Encoder.Encode כותבת שורה חדשה אחרי כל ערך. כברירת מחדל גם Marshal וגם Encoder מבצעים escape ל-<, ל-> ול-& כ-\u003c, \u003e ו-\u0026, כך שבטוח להטמיע את ה-JSON ב-HTML. SetEscapeHTML(false) מכבה את זה. ב-handlers של HTTP, json.NewDecoder(r.Body).Decode(&v) ו-json.NewEncoder(w).Encode(v) הם הצמד הרגיל.

קידוד מותאם אישית

טיפוס יכול לשלוט ב-JSON שלו בעצמו על ידי מימוש json.Marshaler ו-json.Unmarshaler. מקרה נפוץ הוא enum עם iota שצריך להופיע כשם ולא כמספר:

ל-MarshalJSON יש value receiver, כך שהיא עובדת גם לערכים וגם ל-pointers; UnmarshalJSON צריכה pointer receiver כי היא משנה את הערך. טיפוסים שצריכים רק צורת מחרוזת יכולים לממש במקום זה את encoding.TextMarshaler (MarshalText), וזה גם מאפשר להשתמש בהם כמפתחות של map.

טעויות נפוצות

  • שמות שדות באותיות קטנות. מדלגים עליהם בשקט.
  • העברת ערך ל-Unmarshal. היא צריכה pointer.
  • התעלמות מהשגיאה. JSON פגום ואי התאמות בטיפוסים מדווחים רק דרכה.
  • הנחה שמספרים ב-map[string]any הם int. הם float64.
  • ציפייה ש-omitempty ישמיט struct ריק או time.Time באפס. השתמשו ב-omitzero ב-Go 1.24, או ב-pointer.
  • הסתמכות על סדר השדות ב-maps. מפתחות של map ממוינים בפלט; שדות של struct שומרים על סדר ההצהרה שלהם.

שאלות נפוצות

איך ממירים struct ל-JSON ב-Go?

קראו ל-json.Marshal(v), שמחזירה []byte ושגיאה. רק שדות מיוצאים (שמות שמתחילים באות גדולה) נכללים. השתמשו ב-struct tag כמו json:"name" על שדה כדי לקבוע את המפתח שלו ב-JSON. לפלט עם הזחה השתמשו ב-json.MarshalIndent(v, "", " ").

למה שדות ה-struct שלי חסרים בפלט ה-JSON?

encoding/json רואה רק שדות מיוצאים. שדה בשם name (באות קטנה) בלתי נראה בשבילו, גם ב-marshal וגם ב-unmarshal. כתבו את השדה באות גדולה וקבעו את מפתח ה-JSON עם tag כמו json:"name".

מה omitempty עושה ב-JSON של Go?

omitempty משמיט שדה מהפלט כשהוא מחזיק ערך ריק: false, 0, "", pointer או interface שהם nil, או slice או map ריקים. הוא לא מתייחס ל-struct או ל-time.Time כריקים. Go 1.24 הוסיפה את omitzero, שמשמיט כל ערך שהוא ערך האפס של הטיפוס שלו (או שהמתודה IsZero() שלו מחזירה true), כולל structs ו-time.Time.

איך מפענחים JSON עם מבנה לא ידוע ב-Go?

עשו unmarshal לתוך map[string]any (או any). אובייקטים הופכים ל-map[string]any, מערכים ל-[]any, מחרוזות ל-string, בוליאנים ל-bool, וכל מספר ל-float64. השתמשו ב-type assertions כדי לקרוא את הערכים, וב-Decoder.UseNumber אם מספרים שלמים גדולים חייבים להישאר מדויקים.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל