JSON in Go: Marshal, Unmarshal, Struct Tags and Streaming
Convert between JSON and structs, control output with tags, stream with Encoder and Decoder, and avoid the common encoding/json pitfalls.
On this page
JSON is the language of web APIs, and Go's encoding/json package converts between JSON and Go structs with very little code. Struct tags control the field names, and a handful of rules decide what gets encoded.
Struct to JSON: Marshal
json.Marshal turns a Go value into JSON bytes:
type User struct {
Name string `json:"name"`
Department string `json:"department"`
}
u := User{Name: "Sabbir", Department: "CSE"}
data, err := json.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data)) // {"name":"Sabbir","department":"CSE"}Marshal returns []byte. Printing the raw slice shows numbers ([123 34 110 ...]), so convert with string(data) to read it. Use json.MarshalIndent(u, "", " ") for pretty output.
JSON to struct: Unmarshal
var u2 User
err := json.Unmarshal([]byte(`{"name":"ABC","department":"EEE","extra":1}`), &u2)
fmt.Printf("%+v %v\n", u2, err) // {Name:ABC Department:EEE} <nil>Pass a pointer (&u2) so Unmarshal can fill it in. Unknown fields such as extra are silently ignored, and missing fields keep their zero values.
Struct tags
The tag after each field controls its JSON name and behaviour:
type User struct {
Name string `json:"name"` // rename
Email string `json:"email,omitempty"` // drop when empty
Password string `json:"-"` // never encode
Tags []string `json:"tags"`
age int // unexported: always ignored
}
u := User{Name: "Sabbir", Password: "secret"}
data, _ := json.Marshal(u)
fmt.Println(string(data)) // {"name":"Sabbir","tags":null}omitempty skips zero values ("", 0, false, nil, empty slices and maps). Go 1.24 added omitzero, which also handles zero structs like time.Time{} correctly."-" keeps secrets out of API responses.encoding/json, because it uses reflection from another package. A struct with only lowercase fields encodes to {}, a classic beginner bug.nil slice vs empty slice
Notice "tags":null above. A nil slice encodes as null, while an empty slice encodes as []. Frontend code that does user.tags.map(...) crashes on null, so initialise slices you return from APIs:
u.Tags = []string{} // encodes as "tags":[]Streaming: Encoder and Decoder
Marshal/Unmarshal work with byte slices in memory. json.NewEncoder and json.NewDecoder work with streams (io.Writer/io.Reader): HTTP bodies, files, network connections.
// Decode from any reader, for example an HTTP request body
dec := json.NewDecoder(strings.NewReader(`{"name":"X","unknown":true}`))
dec.DisallowUnknownFields() // strict mode for APIs
var u User
err := dec.Decode(&u)
fmt.Println(err) // json: unknown field "unknown"
// Encode to any writer, for example an http.ResponseWriter
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(User{Name: "Sabbir"})
fmt.Printf("%q\n", buf.String()) // "{\"name\":\"Sabbir\",\"tags\":null}\n"Encoder.Encode adds a trailing newline. In HTTP handlers prefer json.NewDecoder(r.Body).Decode(&v) and json.NewEncoder(w).Encode(v): no intermediate byte slice.
DisallowUnknownFields() turns typos in client requests ("emial") into clear errors instead of silently ignored fields.Field name matching is case-insensitive
When decoding, Go matches JSON keys to struct fields case-insensitively. In the original example, {"Name": "ABC", "Class": "7"} still filled fields tagged json:"name" and json:"class". Convenient, but be aware of it when two keys differ only by case.
Unknown shapes: map[string]any
When you cannot define a struct in advance, decode into map[string]any. Be careful with the types you get back:
var v map[string]any
json.Unmarshal([]byte(`{"n":1,"ok":true,"list":[1,"a"]}`), &v)
fmt.Printf("%T %T %T\n", v["n"], v["ok"], v["list"]) // float64 bool []interface {}All JSON numbers become `float64`, which loses precision for large IDs. Use dec.UseNumber() to get json.Number instead, or better, decode into a struct whenever you know the shape.
Custom encoding
Implement json.Marshaler / json.Unmarshaler (or encoding.TextMarshaler) to control how a type is represented. A typical use is encoding an iota enum as a readable string (see the enums article):
func (s OrderStatus) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}Now OrderStatus values appear as "Shipped" in JSON instead of 1.
Key takeaways
Marshal/Unmarshal for byte slices; NewEncoder/NewDecoder for streams.json:"name"), omit (omitempty) or hide (json:"-").nil slices become null; initialise them if clients expect [].DisallowUnknownFields for strict APIs and pass pointers when decoding.map[string]any turns numbers into float64; prefer structs.Last updated on