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:

GO
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))
}
Output
{"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:

GO
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)
}
Output
{ID:1042 Customer:Ana Total:99.95 Paid:false}
error: json: cannot unmarshal string into Go struct field Order.id of type int

Type 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

TagEffect
`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"
GO
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))
}
Output
{"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:

GO
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)
}
Output
name: gopher (string)
age: 16 (float64)
langs: [go c] ([]interface {})
meta: <nil> (<nil>)
17

Working 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:

GO
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)
}
Output
{
 "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:

GO
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))
}
Output
{"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:

GO
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)
}
Output
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: true

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

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
[
  {
    "title": "Dune",
    "year": 1965,
    "read": true
  },
  {
    "title": "Emma",
    "year": 1815
  },
  {
    "title": "Neuromancer",
    "year": 1984
  }
]
Next: dates, times and durations.