Marshal and Unmarshal Basics
Basic Marshal and Unmarshalgo
1package main23import (4 "encoding/json"5 "fmt"6)78type User struct {9 ID int `json:"id"`10 Name string `json:"name"`11 Email string `json:"email"`12 IsActive bool `json:"is_active"`13 Roles []string `json:"roles"`14}1516func main() {17 // Marshal: Go struct -> JSON18 user := User{19 ID: 1, Name: "Alice", Email: "[email protected]",20 IsActive: true, Roles: []string{"admin", "editor"},21 }2223 data, err := json.Marshal(user)24 if err != nil {25 panic(err)26 }27 fmt.Println(string(data))28 // {"id":1,"name":"Alice","email":"[email protected]","is_active":true,"roles":["admin","editor"]}2930 // MarshalIndent: pretty-printed JSON31 pretty, _ := json.MarshalIndent(user, "", " ")32 fmt.Println(string(pretty))3334 // Unmarshal: JSON -> Go struct35 jsonStr := `{"id":2,"name":"Bob","email":"[email protected]","is_active":false,"roles":["viewer"]}`36 var parsed User37 if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {38 panic(err)39 }40 fmt.Printf("Parsed: %+v\n", parsed)41}Handling Dynamic JSON
interface{} for Unknown Structure
Parsing unknown JSONgo
1var result interface{}2json.Unmarshal([]byte(`{"name":"Alice","age":30,"nested":{"key":"val"}}`), &result)34// Type assertion to access fields5m := result.(map[string]interface{})6name := m["name"].(string) // "Alice"7age := m["age"].(float64) // 30.0 (all JSON numbers become float64)8nested := m["nested"].(map[string]interface{})9key := nested["key"].(string) // "val"Note
All JSON numbers become
float64 when unmarshaling into interface{}. This can lose precision for large integers. Use json.Decoder with UseNumber() to preserve numeric precision as a json.Number string type.json.RawMessage for Deferred Parsing
Polymorphic JSON with RawMessagego
1type Event struct {2 Type string `json:"type"`3 Payload json.RawMessage `json:"payload"` // parsed later based on Type4}56type UserCreated struct {7 UserID int `json:"user_id"`8 Email string `json:"email"`9}1011type OrderPlaced struct {12 OrderID int `json:"order_id"`13 Total float64 `json:"total"`14}1516func parseEvent(data []byte) {17 var event Event18 json.Unmarshal(data, &event)1920 switch event.Type {21 case "user.created":22 var payload UserCreated23 json.Unmarshal(event.Payload, &payload)24 fmt.Printf("New user: %s\n", payload.Email)25 case "order.placed":26 var payload OrderPlaced27 json.Unmarshal(event.Payload, &payload)28 fmt.Printf("Order total: $%.2f\n", payload.Total)29 }30}Custom MarshalJSON and UnmarshalJSON
Custom time formatgo
1type CustomTime struct {2 time.Time3}45const layout = "2006-01-02" // Go's reference time format67func (ct CustomTime) MarshalJSON() ([]byte, error) {8 return json.Marshal(ct.Format(layout))9}1011func (ct *CustomTime) UnmarshalJSON(data []byte) error {12 var s string13 if err := json.Unmarshal(data, &s); err != nil {14 return err15 }16 t, err := time.Parse(layout, s)17 if err != nil {18 return err19 }20 ct.Time = t21 return nil22}2324type Event struct {25 Name string `json:"name"`26 Date CustomTime `json:"date"`27}2829// Marshal: {"name":"Launch","date":"2026-04-15"}30// Unmarshal: parses "2026-04-15" back into time.TimeCustom enum serializationgo
1type Status int23const (4 StatusPending Status = iota // 05 StatusActive // 16 StatusInactive // 27)89var statusNames = map[Status]string{10 StatusPending: "pending", StatusActive: "active", StatusInactive: "inactive",11}1213var statusValues = map[string]Status{14 "pending": StatusPending, "active": StatusActive, "inactive": StatusInactive,15}1617func (s Status) MarshalJSON() ([]byte, error) {18 name, ok := statusNames[s]19 if !ok {20 return nil, fmt.Errorf("unknown status: %d", s)21 }22 return json.Marshal(name)23}2425func (s *Status) UnmarshalJSON(data []byte) error {26 var name string27 if err := json.Unmarshal(data, &name); err != nil {28 return err29 }30 val, ok := statusValues[name]31 if !ok {32 return fmt.Errorf("unknown status: %q", name)33 }34 *s = val35 return nil36}3738// {"status":"active"} instead of {"status":1}Streaming with json.Decoder
For large JSON payloads or HTTP request bodies, use json.Decoder instead of json.Unmarshal. The decoder reads from an io.Reader and doesn't need the entire payload in memory:
HTTP handler with json.Decodergo
1func createUserHandler(w http.ResponseWriter, r *http.Request) {2 var req CreateUserRequest3 decoder := json.NewDecoder(r.Body)4 decoder.DisallowUnknownFields() // strict: reject extra fields56 if err := decoder.Decode(&req); err != nil {7 http.Error(w, `{"error":"invalid JSON: `+err.Error()+`"}`, http.StatusBadRequest)8 return9 }1011 // Validate12 if req.Email == "" {13 http.Error(w, `{"error":"email is required"}`, http.StatusBadRequest)14 return15 }1617 // Process...18 user := processUser(req)1920 // Respond with JSON21 w.Header().Set("Content-Type", "application/json")22 json.NewEncoder(w).Encode(user)23}2425// json.NewDecoder reads from r.Body (io.Reader) — no ioutil.ReadAll needed26// json.NewEncoder writes directly to w (io.Writer) — no Marshal + w.Write neededStreaming JSON Lines (NDJSON)
Processing JSON Lines from a large filego
1func processLogs(r io.Reader) error {2 decoder := json.NewDecoder(r)3 for decoder.More() {4 var entry LogEntry5 if err := decoder.Decode(&entry); err != nil {6 return fmt.Errorf("decode error: %w", err)7 }8 // Process each log entry without loading entire file9 handleLogEntry(entry)10 }11 return nil12}Error Handling
Handling different JSON error typesgo
1var user User2err := json.Unmarshal(data, &user)3if err != nil {4 var syntaxErr *json.SyntaxError5 var typeErr *json.UnmarshalTypeError67 switch {8 case errors.As(err, &syntaxErr):9 fmt.Printf("Syntax error at byte offset %d\n", syntaxErr.Offset)10 case errors.As(err, &typeErr):11 fmt.Printf("Type mismatch: field %q expects %s, got %s\n",12 typeErr.Field, typeErr.Type, typeErr.Value)13 default:14 fmt.Printf("JSON error: %v\n", err)15 }16}Performance Alternatives
| Library | Speedup vs stdlib | Approach | Compatibility |
|---|---|---|---|
| encoding/json | 1x (baseline) | Reflection-based | Standard library, always available |
| go-json | ~2-3x | Optimized reflection | Drop-in replacement, import path change only |
| sonic | ~5-6x | JIT compilation (amd64) | Near drop-in, amd64 Linux/macOS only |
| easyjson | ~4-5x | Code generation | Requires running easyjson generator tool |
| jsoniter | ~2-3x | Optimized reflection | ConfigCompatibleWithStandardLibrary mode |
Using go-json as a drop-in replacementgo
1// Change import from "encoding/json" to "github.com/goccy/go-json"2import json "github.com/goccy/go-json"34// All existing code works unchanged5data, err := json.Marshal(user)6err = json.Unmarshal(data, &user)78// Also works with json.NewDecoder/NewEncoder9decoder := json.NewDecoder(r.Body)10encoder := json.NewEncoder(w)Tip
Profile before optimizing. For most web services,
encoding/json is fast enough. Switch to alternatives only when benchmarks show JSON serialization is a bottleneck (typically at thousands of requests per second with large payloads).Real-World Example: GitHub Webhook
Parsing a GitHub push webhookgo
1type GitHubPushEvent struct {2 Ref string `json:"ref"`3 Before string `json:"before"`4 After string `json:"after"`5 Repository Repository `json:"repository"`6 Commits []Commit `json:"commits"`7 Pusher GitHubUser `json:"pusher"`8}910type Repository struct {11 ID int `json:"id"`12 Name string `json:"name"`13 FullName string `json:"full_name"`14 Private bool `json:"private"`15 HTMLURL string `json:"html_url"`16}1718type Commit struct {19 ID string `json:"id"`20 Message string `json:"message"`21 Timestamp string `json:"timestamp"`22 Author Author `json:"author"`23 Added []string `json:"added"`24 Removed []string `json:"removed"`25 Modified []string `json:"modified"`26}2728type Author struct {29 Name string `json:"name"`30 Email string `json:"email"`31}3233type GitHubUser struct {34 Name string `json:"name"`35 Email string `json:"email"`36}3738func handleWebhook(w http.ResponseWriter, r *http.Request) {39 var event GitHubPushEvent40 if err := json.NewDecoder(r.Body).Decode(&event); err != nil {41 http.Error(w, "invalid webhook payload", http.StatusBadRequest)42 return43 }44 fmt.Printf("Push to %s: %d commits by %s\n",45 event.Repository.FullName, len(event.Commits), event.Pusher.Name)46 w.WriteHeader(http.StatusOK)47}Common Gotchas
- ✗Unexported fields (lowercase) are silently ignored — always export fields or use struct tags
- ✗All numbers become
float64when unmarshaling tointerface{}— useUseNumber()for precision - ✗
omitemptyhides legitimate zero values (0, false) — use pointers for nullable fields - ✗Unknown fields are silently ignored by default — use
DisallowUnknownFields()for strict parsing - ✗
time.Timemarshals to RFC 3339 by default — implement custom MarshalJSON for other formats - ✗A nil slice marshals to
null, not[]— initialize withmake([]T, 0)if you need empty arrays
Try These Tools
Continue Learning
Frequently Asked Questions
How do I parse JSON in Go?
Use json.Unmarshal([]byte(jsonString), &target) where target is a pointer to a struct or interface{}. Define a Go struct with fields matching the JSON keys and use struct tags (e.g., `json:"field_name"`) to map JSON keys to Go field names. Only exported (uppercase) fields are included in JSON operations.
Why are my Go struct fields missing from JSON output?
Only exported fields (starting with an uppercase letter) are included in JSON marshaling. If your field is named "name" (lowercase), change it to "Name" and use a struct tag `json:"name"` to keep the JSON key lowercase. Unexported fields are silently ignored by encoding/json.
What does omitempty do in Go JSON struct tags?
The omitempty option causes a field to be omitted from JSON output when it has its zero value: 0 for numbers, "" for strings, false for booleans, nil for pointers/slices/maps, and empty for structs. Use it as `json:"field,omitempty"`. Note: a non-nil empty slice and a nil slice both produce different JSON ([] vs omitted).
How do I handle unknown JSON fields in Go?
By default, json.Unmarshal silently ignores unknown fields. To catch them, use json.NewDecoder(reader).DisallowUnknownFields() which returns an error if the JSON contains keys not present in the target struct. This is useful for strict API validation.
How do I parse JSON with dynamic or unknown structure in Go?
Use interface{} (or any in Go 1.18+) as the target type. JSON objects become map[string]interface{}, arrays become []interface{}, strings become string, and numbers become float64. For partial parsing, use json.RawMessage to defer decoding of specific fields.
What is json.RawMessage used for?
json.RawMessage is a raw encoded JSON value that delays parsing. Use it when a field can contain different JSON structures (polymorphic types) — first unmarshal the outer structure with RawMessage for the dynamic field, then inspect a type discriminator to decide which concrete struct to unmarshal the raw JSON into.
Is Go encoding/json slow? What are faster alternatives?
encoding/json uses reflection and is slower than code-generation approaches. Faster alternatives: sonic (~6x faster, uses JIT on amd64), go-json (~2-3x faster, drop-in replacement), easyjson (code-gen, ~5x faster but requires running a generator). For most applications, encoding/json is fast enough — optimize only when profiling shows it is a bottleneck.