📦 Analogy
JSON is a shipping container for data: Marshal packs a Go value into the standard box so it can travel over a network or into a file, and Unmarshal unpacks a box back into a Go value. Struct tags are the packing slip — they say which Go field maps to which label on the box, and which items to leave out.
Marshal and Unmarshal
json.Marshal turns a Go value into []byte of JSON; json.Unmarshal fills a Go value from JSON bytes. The mapping between Go fields and JSON keys is driven by struct tags — backtick-quoted metadata after each field. Only exported (capitalized) fields are visible to the reflection that json uses, so unexported fields are silently skipped:
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"` // dropped if ""
Age int `json:"age"`
Token string `json:"-"` // never marshaled
pass string // unexported: invisible to json
}
Run a full round-trip. (The playground writes the tags as plain double-quoted strings, which behave identically to backtick tags at runtime.)
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string "json:\"name\""
Email string "json:\"email,omitempty\""
Age int "json:\"age\""
pass string // unexported: never marshaled
}
func main() {
u := User{Name: "Ada", Age: 36, pass: "secret"}
b, _ := json.Marshal(u)
fmt.Println(string(b)) // {"name":"Ada","age":36} — email omitted, pass skipped
pretty, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(pretty))
// decode JSON back into a struct (Unmarshal needs a pointer)
src := "{\"name\":\"Bob\",\"email\":\"b@x.io\",\"age\":40,\"extra\":true}"
var got User
if err := json.Unmarshal([]byte(src), &got); err != nil {
fmt.Println("error:", err)
}
fmt.Printf("%+v\n", got) // unknown "extra" key is silently ignored
}
Known shape? Use a struct. Unknown? Use a map
When you know the structure, decode into a typed struct — it’s safe, self-documenting, and gives you real Go types. When the shape is dynamic, decode into map[string]any and you get a generic tree: every object becomes a map[string]any, every array a []any, and every number a float64 (JSON has no integer type):
graph LR J["JSON bytes"] -->|"Unmarshal into &struct"| S["typed struct — you know the shape"] J -->|"Unmarshal into &map[string]any"| M["generic tree: object→map, array→[]any, number→float64, null→nil"]
package main
import (
"encoding/json"
"fmt"
)
func main() {
src := []byte("{\"id\":7,\"tags\":[\"a\",\"b\"],\"meta\":{\"ok\":true}}")
var tree map[string]any
json.Unmarshal(src, &tree)
// numbers arrive as float64 — assert and convert
id := int(tree["id"].(float64))
tags := tree["tags"].([]any)
meta := tree["meta"].(map[string]any)
fmt.Println("id:", id) // 7
fmt.Println("first tag:", tags[0]) // a
fmt.Println("ok:", meta["ok"]) // true
}
Streaming with Encoder and Decoder
Marshal/Unmarshal work on a []byte held entirely in memory. For an io.Reader or io.Writer — an HTTP body, a file, a socket — use json.NewDecoder(r) and json.NewEncoder(w) to stream without buffering the whole document:
package main
import (
"bytes"
"encoding/json"
"fmt"
"strings"
)
type Item struct {
Name string "json:\"name\""
Qty int "json:\"qty\""
}
func main() {
// Decoder reads from any io.Reader and can consume a STREAM of values.
stream := strings.NewReader("{\"name\":\"pen\",\"qty\":3}\n{\"name\":\"ink\",\"qty\":1}")
dec := json.NewDecoder(stream)
for dec.More() {
var it Item
dec.Decode(&it)
fmt.Printf("%s x%d\n", it.Name, it.Qty)
}
// Encoder writes to any io.Writer (here a buffer; in a server, w).
var out bytes.Buffer
enc := json.NewEncoder(&out)
enc.SetIndent("", " ")
enc.Encode(Item{Name: "pad", Qty: 5})
fmt.Print(out.String())
}
In an HTTP handler this is the idiomatic pattern: json.NewDecoder(r.Body).Decode(&req) to read, json.NewEncoder(w).Encode(resp) to write.
Custom encoding: Marshaler and RawMessage
When the default field-by-field mapping isn’t what the wire format needs, a type can implement json.Marshaler (MarshalJSON() ([]byte, error)) and json.Unmarshaler (UnmarshalJSON([]byte) error) to control its own representation — a date as a Unix epoch, an enum as a string, a money value as cents. And json.RawMessage lets you defer decoding part of a document: keep the raw bytes now, decode them once you know which concrete type they are.
package main
import (
"encoding/json"
"fmt"
)
// Status marshals as a name, not a number.
type Status int
const (
Active Status = iota
Banned
)
func (s Status) MarshalJSON() ([]byte, error) {
return []byte("\"" + [...]string{"active", "banned"}[s] + "\""), nil
}
type Account struct {
User string "json:\"user\""
Status Status "json:\"status\""
Extra json.RawMessage "json:\"extra\"" // kept raw, decoded later
}
func main() {
a := Account{User: "ada", Status: Banned, Extra: json.RawMessage("{\"k\":1}")}
b, _ := json.Marshal(a)
fmt.Println(string(b)) // {"user":"ada","status":"banned","extra":{"k":1}}
}
Common edge cases
- Numbers decode to
float64inside anany, so largeint64IDs can lose precision. Decode into a typedint64field, or calldec.UseNumber()to get ajson.Number(a string you parse exactly). - Missing vs zero. A plain
intfield can’t tell “key absent” from"x":0. Use a pointer (*int) — nil means absent. - Unknown keys are ignored by default. Call
dec.DisallowUnknownFields()to make extra keys an error (good for strict config parsing). time.Timemarshals/unmarshals as RFC 3339 automatically (it implements the interfaces).- HTML escaping.
Marshalescapes<,>,&to<etc. for safe embedding in HTML; disable withenc.SetEscapeHTML(false). - Maps sort keys. Marshaling a map produces keys in sorted order, so output is deterministic.
⚠️ Unmarshal needs a pointer, and is lenient by design
Pass a pointer to Unmarshal (&u) — it has to write into your value; passing a non-pointer silently does nothing useful. Decoding is deliberately forgiving: JSON keys with no matching field are dropped, fields absent from the JSON keep their zero value, and type mismatches return an error mid-decode (leaving earlier fields populated). So always check the returned error and then validate the result — JSON parsing succeeding does not mean the data is complete or sane.
See also
- fmt & io —
Encoder/Decoderare built onio.Writer/io.Reader. - structs — struct tags, exported fields, and embedding (which flattens into JSON).
- reflection — the mechanism
encoding/jsonuses to read tags and fields at runtime. - REST APIs — decoding request bodies and encoding responses in handlers.
🐞 Fix the bug
Marshal runs without an error here — and produces {}. The struct’s fields are invisible to encoding/json. Edit until Run & check matches.
encoding/json silently skips unexported fields, so nothing is emitted. Make it output both fields — with lowercase keys.
{"name":"Gopher","age":13}package main
import (
"encoding/json"
"fmt"
)
type user struct {
name string
age int
}
func main() {
u := user{name: "Gopher", age: 13}
data, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
Next: generating text and safe HTML from data — templates.
Related topics
Check your understanding
Score: 0 / 51. Why must a struct field be exported (capitalized) to be marshaled to JSON?
json.Marshal reflects over the value, and reflection can't read unexported (lowercase) fields. They're silently skipped. Use a json:"name" tag to control the key's casing.
2. What does the `omitempty` option in a json struct tag do?
The tag's first part renames the JSON key; omitempty drops the field from output when it's the zero value (empty string, 0, nil, …). Use json:"-" to skip a field entirely.
3. You Unmarshal arbitrary JSON whose shape you don't know. What type do you decode into?
Decoding into any/map[string]any gives a generic tree: objects become map[string]any, arrays []any, numbers float64. Prefer a typed struct when you know the shape.
4. After json.Unmarshal, how do you tell a field that was absent from one explicitly set to its zero value (e.g. 0)?
A plain int can't distinguish missing from 0 — both end up 0. A *int is left nil when the key is absent and points to 0 when the JSON says "x":0. Pointers (or json.RawMessage / a custom Unmarshaler) are how you detect presence.
5. Why might you implement json.Marshaler on a type?
Implementing MarshalJSON() ([]byte, error) lets a type define exactly how it serializes — a custom date format, an enum-as-string, a wrapped value. The matching UnmarshalJSON does the reverse for decoding.
Comments
Sign in with GitHub to join the discussion.