JSON
JSON is the common language of web APIs and config files, and Go's encoding/json package converts between JSON text and Go values. Structs with field tags map neatly to JSON objects, and decoding into a struct checks types as it goes. This lesson covers encoding and decoding, field tags, unknown and missing fields, decoding arbitrary JSON, streaming, custom formats, and the new encoding/json/v2 package in Go 1.27.
Encoding: Marshal
json.Marshal turns a Go value into JSON bytes. Struct fields must be exported (capitalised) to be included; tags set the JSON names:
package main import ( "encoding/json" "fmt" ) type Product struct { ID int `json:"id"` Name string `json:"name"` Price float64 `json:"price"` Tags []string `json:"tags"` InStock bool `json:"in_stock"` internal string // unexported: never encoded } func main() { p := Product{ID: 7, Name: "Desk lamp", Price: 34.5, Tags: []string{"home", "light"}, InStock: true} data, err := json.Marshal(p) if err != nil { fmt.Println(err) return } fmt.Println(string(data)) pretty, _ := json.MarshalIndent(p, "", " ") fmt.Println(string(pretty)) }
{"id":7,"name":"Desk lamp","price":34.5,"tags":["home","light"],"in_stock":true}
{
"id": 7,
"name": "Desk lamp",
"price": 34.5,
"tags": [
"home",
"light"
],
"in_stock": true
}Decoding: Unmarshal
json.Unmarshal fills in a value you pass by pointer. Fields missing from the JSON keep their zero values, and JSON members with no matching field are ignored:
package main import ( "encoding/json" "fmt" ) type Order struct { ID int `json:"id"` Customer string `json:"customer"` Total float64 `json:"total"` Paid bool `json:"paid"` } func main() { input := `{"id": 1042, "customer": "Ana", "total": 99.95, "coupon": "SAVE10"}` var o Order if err := json.Unmarshal([]byte(input), &o); err != nil { fmt.Println("error:", err) return } fmt.Printf("%+v\n", o) err := json.Unmarshal([]byte(`{"id": "not a number"}`), &o) fmt.Println("error:", err) }
{ID:1042 Customer:Ana Total:99.95 Paid:false}
error: json: cannot unmarshal string into Go struct field Order.id of type intType mismatches are reported as errors, so decoding into a struct doubles as basic validation. Paid is false simply because it wasn't present; if you need to tell "false" from "missing", make the field a pointer (*bool), which stays nil when absent.
Tag options
| Tag | Effect |
|---|---|
`json:"name"` | Use name as the JSON key |
`json:"-"` | Never encode or decode this field |
`json:",omitempty"` | Leave out when false, 0, "", nil, or an empty slice or map |
`json:",omitzero"` | Leave out when the value is its zero value (or its IsZero method says so) — works for structs like time.Time |
`json:",string"` | Encode a number or bool inside a JSON string, e.g. "42" |
package main import ( "encoding/json" "fmt" "time" ) type Event struct { Name string `json:"name"` Note string `json:"note,omitempty"` Attendees []string `json:"attendees,omitempty"` Start time.Time `json:"start,omitzero"` ID int64 `json:"id,string"` Secret string `json:"-"` } func main() { a, _ := json.Marshal(Event{Name: "launch", ID: 9007199254740993, Secret: "x"}) fmt.Println(string(a)) b, _ := json.Marshal(Event{Name: "standup", Note: "daily", Start: time.Date(2026, 10, 8, 9, 30, 0, 0, time.UTC)}) fmt.Println(string(b)) }
{"name":"launch","id":"9007199254740993"}
{"name":"standup","note":"daily","start":"2026-10-08T09:30:00Z","id":"0"}Very large integer IDs are often sent as strings because JavaScript can't represent every 64-bit integer exactly; the string option handles that. time.Time values encode as RFC 3339 text.
Arbitrary JSON
When the structure isn't known in advance, decode into map[string]any or any. Objects become maps, arrays become []any, and all numbers become float64:
package main import ( "encoding/json" "fmt" ) func main() { var data map[string]any json.Unmarshal([]byte(`{"name": "gopher", "age": 16, "langs": ["go", "c"], "meta": null}`), &data) for _, k := range []string{"name", "age", "langs", "meta"} { fmt.Printf("%s: %v (%T)\n", k, data[k], data[k]) } age := data["age"].(float64) fmt.Println(int(age) + 1) }
name: gopher (string)
age: 16 (float64)
langs: [go c] ([]interface {})
meta: <nil> (<nil>)
17Working with any requires type assertions everywhere, so define structs for the parts you care about whenever you can. json.RawMessage lets you delay decoding a part until you know its shape.
Streaming: Encoder and Decoder
json.NewEncoder(w) writes JSON to any io.Writer, and json.NewDecoder(r) reads from any io.Reader — files, HTTP bodies, network connections. A decoder can read a stream of several JSON values one after another:
package main import ( "encoding/json" "errors" "fmt" "io" "os" "strings" ) type Reading struct { Sensor string `json:"sensor"` Value float64 `json:"value"` } func main() { stream := `{"sensor":"t1","value":21.5} {"sensor":"t2","value":19.0} {"sensor":"t1","value":22.1}` dec := json.NewDecoder(strings.NewReader(stream)) sum := map[string]float64{} for { var r Reading err := dec.Decode(&r) if errors.Is(err, io.EOF) { break } if err != nil { fmt.Println("bad record:", err) return } sum[r.Sensor] += r.Value } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(sum) }
{
"t1": 43.6,
"t2": 19
}To reject JSON with unexpected fields — useful for config files, where a typo should be an error — call dec.DisallowUnknownFields() before decoding.
Custom encoding
A type controls its own JSON form by implementing MarshalJSON and UnmarshalJSON. For types with a natural text form, MarshalText/UnmarshalText are simpler, and also work for map keys:
package main import ( "encoding/json" "fmt" "strings" ) type Level int const ( Low Level = iota High ) func (l Level) MarshalText() ([]byte, error) { return []byte([]string{"low", "high"}[l]), nil } func (l *Level) UnmarshalText(b []byte) error { switch strings.ToLower(string(b)) { case "low": *l = Low case "high": *l = High default: return fmt.Errorf("unknown level %q", b) } return nil } type Alert struct { Msg string `json:"msg"` Level Level `json:"level"` } func main() { out, _ := json.Marshal(Alert{"disk full", High}) fmt.Println(string(out)) var a Alert fmt.Println(json.Unmarshal([]byte(`{"msg":"cpu","level":"LOW"}`), &a), a) fmt.Println(json.Unmarshal([]byte(`{"msg":"?","level":"extreme"}`), &a)) }
{"msg":"disk full","level":"high"}
<nil> {cpu 0}
unknown level "extreme"encoding/json/v2
Go 1.27 adds encoding/json/v2, a revised JSON package with stricter, more predictable defaults and options passed to each call. (The original encoding/json now runs on the same engine but keeps its old behaviour, so existing code doesn't need to change.) Some differences in the defaults:
package main import ( "encoding/json" jsonv2 "encoding/json/v2" "fmt" ) type User struct { Name string `json:"name"` Roles []string `json:"roles"` } func main() { var u1, u2 User input := []byte(`{"NAME": "ana"}`) json.Unmarshal(input, &u1) jsonv2.Unmarshal(input, &u2) fmt.Printf("v1 %q, v2 %q (v2 matches names case-sensitively)\n", u1.Name, u2.Name) a, _ := json.Marshal(User{Name: "ben"}) b, _ := jsonv2.Marshal(User{Name: "ben"}) fmt.Println("v1", string(a), " v2", string(b)) err := jsonv2.Unmarshal([]byte(`{"name":"a","name":"b"}`), &u2) fmt.Println("v2 rejects duplicate names:", err != nil) err = jsonv2.Unmarshal([]byte(`{"name":"c","admin":true}`), &u2, jsonv2.RejectUnknownMembers(true)) fmt.Println("v2 rejects unknown members:", err != nil) }
v1 "ana", v2 "" (v2 matches names case-sensitively)
v1 {"name":"ben","roles":null} v2 {"name":"ben","roles":[]}
v2 rejects duplicate names: true
v2 rejects unknown members: trueThe examples print only whether an error occurred, for a good reason: v2 deliberately varies the wording of its error messages slightly from run to run (running the unknown-member example repeatedly printed both "cannot unmarshal" and "unable to unmarshal"), so that programs can't come to depend on the exact text. Inspect errors with errors.Is and errors.AsType, never by comparing strings.
For new code that needs these stricter rules or the extra options, v2 is worth a look; the familiar v1 API remains fully supported. The rest of this course uses v1, which is what you'll meet in most existing code.
Try it yourself
Decode a list of books from JSON, add one, and encode the list again with indentation.
[
{
"title": "Dune",
"year": 1965,
"read": true
},
{
"title": "Emma",
"year": 1815
},
{
"title": "Neuromancer",
"year": 1984
}
]