JSON and struct tags

Go · Practical · 12 min

JSON is the lingua franca of APIs and config, and struct tags are how Go maps it to types.

The encoding/json package converts between Go values and JSON:

  • json.Marshal(v) returns []byte; json.MarshalIndent pretty-prints.
  • json.Unmarshal(data, &v) decodes into a pointer.
  • json.NewEncoder(w).Encode(v) and json.NewDecoder(r).Decode(&v) stream to and from an io.Writer / io.Reader, such as an HTTP body.

Only exported fields are encoded; lower-case fields are silently ignored — the classic "my JSON is {}" bug. Struct tags customise the mapping:

type User struct {
	ID       int    `json:"id"`
	Email    string `json:"email,omitempty"`
	Password string `json:"-"`
}
  • json:"name" sets the key; - skips the field.
  • omitempty drops zero values (empty string, 0, nil, empty slice or map). It doesn't treat an empty struct as empty. Newer Go versions (1.24+) add omitzero for that.
  • Matching on decode is case-insensitive, and unknown keys are ignored unless you call dec.DisallowUnknownFields().

Type mapping: objects decode into structs or map[string]any, arrays into slices, numbers into any numeric field — and into float64 when the target is any. Use a pointer field (*int) when you must distinguish "missing" from zero. time.Time encodes as RFC 3339.

For custom formats, implement MarshalJSON/UnmarshalJSON, or encoding.TextMarshaler for values that are naturally strings. json.RawMessage defers decoding of part of a document.

Example

package main

import (
	"encoding/json"
	"fmt"
	"strings"
)

type Order struct {
	ID    int      `json:"id"`
	Items []string `json:"items"`
	Note  string   `json:"note,omitempty"`
	token string   // unexported: never encoded
}

func main() {
	b, _ := json.Marshal(Order{ID: 7, Items: []string{"tea"}, token: "x"})
	fmt.Println(string(b)) // {"id":7,"items":["tea"]}

	var o Order
	dec := json.NewDecoder(strings.NewReader(`{"id": 8, "items": ["a","b"], "note": "rush"}`))
	if err := dec.Decode(&o); err != nil {
		fmt.Println("decode:", err)
		return
	}
	fmt.Printf("%+v\n", o) // {ID:8 Items:[a b] Note:rush token:}
}

Remember

  • Only exported fields are (un)marshalled
  • Struct tags set names: json:"name,omitempty", json:"-"
  • Use Decoder/Encoder for streams like HTTP bodies

Watch out for

  • Passing a value instead of a pointer to json.Unmarshal — it returns an InvalidUnmarshalError.

Check yourself

  1. A struct has field `count int` with tag `json:"count"`. What does Marshal output for it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pkg.go.dev/encoding/json