CHAPTER 29 · STANDARD LIBRARY

Encoding JSON

JSON is the lingua franca of modern APIs. Go's encoding/json package handles marshaling (Go → JSON) and unmarshaling (JSON → Go) using struct tags. This chapter covers the happy path and the tricky corners.

Learning objectives

  • json.Marshal / json.Unmarshal for one-shot conversion.
  • Control field names and behavior with struct tags.
  • Use omitempty, -, and ,string tag options.
  • Handle unknown / dynamic JSON with map[string]any or json.RawMessage.
  • Stream large JSON with json.Decoder / json.Encoder.
  • Implement custom marshalers when the defaults don't fit.

Marshal, Go to JSON

type User struct {
    Name  string
    Email string
    Age   int
}

u := User{Name: "Alice", Email: "a@example.com", Age: 30}
b, err := json.Marshal(u)
fmt.Println(string(b))   // {"Name":"Alice","Email":"a@example.com","Age":30}

Only exported fields are marshaled. Unexported fields are silently ignored.

Unmarshal, JSON to Go

var u User
data := []byte(`{"Name":"Bob","Email":"b@example.com","Age":25}`)
if err := json.Unmarshal(data, &u); err != nil { /* handle */ }
fmt.Printf("%+v\n", u)

Pass a pointer to Unmarshal: it writes into your destination. Unknown JSON fields are ignored by default; missing fields get the Go zero value.

Struct tags, controlling JSON field names

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

Now Marshal produces lowercase keys matching typical JSON API conventions.

omitempty, -, and ,string

type User struct {
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"`   // skip if zero value
    Password string `json:"-"`                  // never marshal
    Level    int    `json:"level,string"`       // encode as a JSON string
}

omitempty skips a field whose value is the zero value for its type. Watch out: a legitimate 0 or false will be omitted too. Use a pointer (*int) if you need to distinguish "not set" from "set to zero".

Unknown shapes

// When you don't know the structure:
var v any
json.Unmarshal(data, &v)
// v is a map[string]any with nested maps, slices, strings, floats, bools

For partial parsing, use json.RawMessage on the fields whose structure varies:

type Envelope struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"`   // parsed later based on Type
}

Streaming encoders/decoders

For big inputs or HTTP bodies, use json.Decoder. It reads from an io.Reader:

resp, _ := http.Get(url)
defer resp.Body.Close()

var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil { /* ... */ }

Conversely, json.Encoder writes to an io.Writer: often used to stream an HTTP response:

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

Custom Marshal / Unmarshal

Implement the interface to take full control:

type Duration time.Duration

func (d Duration) MarshalJSON() ([]byte, error) {
    return []byte(fmt.Sprintf("%q", time.Duration(d).String())), nil
}

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil { return err }
    parsed, err := time.ParseDuration(s)
    if err != nil { return err }
    *d = Duration(parsed)
    return nil
}

Now your Duration marshals as "1h30m" instead of nanoseconds.

Pretty printing

b, _ := json.MarshalIndent(u, "", "  ")
fmt.Println(string(b))
// {
//   "name": "Alice",
//   "email": "a@example.com",
//   "age": 30
// }

For Encoder: enc.SetIndent("", " ").

Check your understanding

Practice exercises

EXERCISE 1

Round-trip a struct

Define a Product struct with ID, Name, Price, Tags ([]string). Marshal an instance to JSON, print it, then unmarshal back into a new variable. Verify they're equal with reflect.DeepEqual.

Show solution
package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

type Product struct {
    ID    int      `json:"id"`
    Name  string   `json:"name"`
    Price float64  `json:"price"`
    Tags  []string `json:"tags,omitempty"`
}

func main() {
    p := Product{ID: 1, Name: "Widget", Price: 9.99, Tags: []string{"new", "hot"}}

    b, _ := json.MarshalIndent(p, "", "  ")
    fmt.Println(string(b))

    var p2 Product
    _ = json.Unmarshal(b, &p2)
    fmt.Println("equal?", reflect.DeepEqual(p, p2))    // true
}

Further reading

JSON, any shape.