Go by Example

JSON

json.Marshal and Unmarshal, struct tags with omitempty, streaming with Encoder/Decoder, custom marshalers, and json.RawMessage for deferred decoding.

The encoding/json package encodes Go values to JSON and decodes JSON into Go values. Struct field tags (json:"name") control the mapping between Go field names and JSON keys.

Encode a struct to JSON with json.Marshal. Struct tags map Go field names to JSON keys. Unexported fields are always omitted.

package main
 
import (
    "encoding/json"
    "fmt"
)
 
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"` // omitted if empty
    pass  string // unexported - never included
}
 
func main() {
    u := User{ID: 1, Name: "Alice"}
    b, err := json.Marshal(u)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(b)) // {"id":1,"name":"Alice"}
}

Decode JSON into a struct with json.Unmarshal. Unknown JSON fields are ignored by default; use json.Decoder with DisallowUnknownFields to reject them.

package main
 
import (
    "encoding/json"
    "fmt"
)
 
type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}
 
func main() {
    data := []byte(`{"id":42,"name":"Bob","role":"admin"}`)
 
    var u User
    if err := json.Unmarshal(data, &u); err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", u) // {ID:42 Name:Bob}
    // "role" was ignored
}

Use json.Encoder and json.Decoder for streaming. They write to and read from any io.Writer/io.Reader without buffering the full JSON in memory.

package main
 
import (
    "bytes"
    "encoding/json"
    "fmt"
)
 
type Item struct {
    Name  string `json:"name"`
    Price int    `json:"price"`
}
 
func main() {
    items := []Item{{"Apple", 1}, {"Banana", 2}}
 
    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    enc.SetIndent("", "  ")
    for _, item := range items {
        enc.Encode(item) // writes one JSON object per line
    }
    fmt.Print(buf.String())
 
    // Decode back
    dec := json.NewDecoder(&buf)
    for dec.More() {
        var item Item
        dec.Decode(&item)
        fmt.Printf("%+v\n", item)
    }
}

json.RawMessage defers decoding of part of a JSON payload. Use it when the shape of a nested value depends on a sibling field.

package main
 
import (
    "encoding/json"
    "fmt"
)
 
type Event struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"`
}
 
type ClickPayload struct {
    X, Y int
}
 
func main() {
    raw := []byte(`{"type":"click","payload":{"X":10,"Y":20}}`)
 
    var event Event
    json.Unmarshal(raw, &event)
 
    if event.Type == "click" {
        var click ClickPayload
        json.Unmarshal(event.Payload, &click)
        fmt.Printf("click at (%d, %d)\n", click.X, click.Y)
    }
}

In production

omitempty omits zero values - for strings that is "", for ints 0, for pointers nil. This distinction matters when a client needs to distinguish "field not set" from "field explicitly set to zero". Use *string or a custom nullable type to represent fields that must be explicitly absent vs explicitly empty. For high-throughput services, consider encoding/json/v2 (proposal) or github.com/bytedance/sonic / github.com/json-iterator/go as drop-in replacements with significantly better performance. Always use json.Decoder over json.Unmarshal for HTTP request bodies - it streams from the request body without loading the full payload into a byte slice.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free