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.Unmarshalfor one-shot conversion.- Control field names and behavior with struct tags.
- Use
omitempty,-, and,stringtag options. - Handle unknown / dynamic JSON with
map[string]anyorjson.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.
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
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
encoding/jsonreference- JSON and Go (blog)
- JSON-to-Go converter: drop in sample JSON, get matching struct
JSON, any shape.