Let's Go Further Appendices › JSON decoding nuances
Previous · Contents · Next
Chapter 21.5.

JSON decoding nuances

Decoding into Go arrays

When you’re decoding a JSON array into a Go array (not a slice) there are a couple of important behaviors to be aware of:

As an example:

js := `[1, 2, 3]`

var tooShortArray [2]int
err := json.NewDecoder(strings.NewReader(js)).Decode(&tooShortArray)
if err != nil {
    log.Fatal(err)
}

var tooLongArray [4]int
err = json.NewDecoder(strings.NewReader(js)).Decode(&tooLongArray)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("tooShortArray: %v\n", tooShortArray)
fmt.Printf("tooLongArray: %v\n", tooLongArray)

Will print out:

tooShortArray: [1 2]
tooLongArray: [1 2 3 0]

Partial JSON decoding

If you have a lot of JSON input to process and only need a small part of it, it’s often possible to leverage the json.RawMessage type to help deal with this. For example:

// Let's say that the only thing we're interested in is processing the "genres" array in
// the following JSON object
js := `{"title": "Top Gun", "genres": ["action", "romance"], "year": 1986}`

// Decode the JSON object to a map[string]json.RawMessage type. The json.RawMessage 
// values in the map will retain their original, undecoded, JSON values.
var m map[string]json.RawMessage

err := json.NewDecoder(strings.NewReader(js)).Decode(&m)
if err != nil {
    log.Fatal(err)
}

// We can then access the JSON "genres" value from the map and decode it as normal using
// the json.Unmarshal() function.
var genres []string

err = json.Unmarshal(m["genres"], &genres)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("genres: %v\n", genres)

This will print out:

genres: [action romance]

In this toy example, using json.RawMessage doesn’t save us much work. But if you need to process a JSON object with tens or hundreds of key-value pairs and only need a few of them, then taking this approach can save you a lot of typing.

Decoding into the any type

It’s possible to decode JSON values into an any type. When you do this, the underlying value that the any type holds will depend on the type of the JSON value being decoded.

JSON type Underlying Go type of any
JSON boolean bool
JSON string string
JSON number float64
JSON array []any
JSON object map[string]any
JSON null nil

Decoding into an any type can be useful in situations where:

As an example, consider the following code:

// This JSON array contains both JSON string and JSON boolean types.
js := `["foo", true]`

// Decode the JSON into a []any slice.
var s []any

err := json.NewDecoder(strings.NewReader(js)).Decode(&s)
if err != nil {
    log.Fatal(err)
}

// The first value in the slice will have the underlying Go type string, the second will
// have the underlying Go type bool. We can then type assert them and print them out  
// the values along with their underlying type.
fmt.Printf("item: 0; type: %T; value: %v\n", s[0], s[0].(string))
fmt.Printf("item: 1; type: %T; value: %v\n", s[1], s[1].(bool))

This will print out:

item: 0; type: string; value: foo
item: 1; type: bool; value: true

Decoding a JSON number into an any type

As shown in the table above, when you decode a JSON number into an any type the value will have the underlying type float64 — even if it is an integer in the original JSON. For example:

js := `10` // This JSON number is an integer.

var n any

err := json.NewDecoder(strings.NewReader(js)).Decode(&n)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("type: %T; value: %v\n", n, n) 

Will print:

type: float64; value: 10

If you want to get the value as an integer (instead of a float64) you should call the UseNumber() method on your json.Decoder instance before decoding. This will cause all JSON numbers to be decoded to the underlying type json.Number instead of float64.

The json.Number type then provides an Int64() method that you can call to get the number as an int64, or the String() method to get the number as a string. For example:

js := `10`

var n any

dec := json.NewDecoder(strings.NewReader(js))
dec.UseNumber() // Before using the decoder, call the UseNumber() method on it.
err := dec.Decode(&n)
if err != nil {
    log.Fatal(err)
}

// Type assert the any value to a json.Number, and then call the Int64() method
// to get the number as a Go int64.
nInt64, err := n.(json.Number).Int64()
if err != nil {
    log.Fatal(err)
}

// Likewise, you can use the String() method to get the number as a Go string.
nString := n.(json.Number).String()

fmt.Printf("type: %T; value: %v\n", n, n)
fmt.Printf("type: %T; value: %v\n", nInt64, nInt64)
fmt.Printf("type: %T; value: %v\n", nString, nString)

This will print out:

type: json.Number; value: 10
type: int64; value: 10
type: string; value: 10

Struct tag directives

Using the struct tag json:"-" on a struct field will cause it to be ignored when decoding JSON, even if the JSON input contains a corresponding key-value pair. For example:

js := `{"name": "alice", "age": 21}`

var person struct {
    Name string `json:"name"`
    Age  int32 `json:"-"`
}

err := json.NewDecoder(strings.NewReader(js)).Decode(&person)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("%+v", person)

Will print out:

{Name:alice Age:0}

The omitzero and omitempty struct tag directives do not have any effect on JSON decoding behavior.