{} The Go Reference

Essentials · Stdlib · Beginner

encoding/json

Turning Go values into JSON and back — Marshal/Unmarshal, struct tags and omitempty, decoding into structs vs maps, streaming Encoder/Decoder, custom Marshaler/Unmarshaler, and json.RawMessage for deferred decoding.

Essentials Beginner ⏱ 7 min read Complete

📦 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.)

json.go — editable & runnable
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"]
dynamic.go — editable & runnable
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:

stream.go — editable & runnable
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.

custom.go — editable & runnable
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 float64 inside an any, so large int64 IDs can lose precision. Decode into a typed int64 field, or call dec.UseNumber() to get a json.Number (a string you parse exactly).
  • Missing vs zero. A plain int field 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.Time marshals/unmarshals as RFC 3339 automatically (it implements the interfaces).
  • HTML escaping. Marshal escapes <, >, & to < etc. for safe embedding in HTML; disable with enc.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 & ioEncoder/Decoder are built on io.Writer/io.Reader.
  • structs — struct tags, exported fields, and embedding (which flattens into JSON).
  • reflection — the mechanism encoding/json uses 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.

🐞 marshal.go — fix the bug

encoding/json silently skips unexported fields, so nothing is emitted. Make it output both fields — with lowercase keys.

Expected output
{"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.

Check your understanding

Score: 0 / 5

1. 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.