Unmarshaling a JSON array into a Go struct

Sometimes, you see heterogeneous JSON array like

["Hello world", 10, false]

Dealing with such an array in Go can be very frustrating. A []interface{} hell is just about as painful as the map[string]interface{} hell (See my earlier article about that).

The natural way to deal with data like that in Go would be a struct like

type Notification struct {
	Message  string
	Priority uint8
	Critical bool
}

See how much more meaning we've added?

Now, you can't just json.Unmarshal an array into a struct. I'll show you how to make that work.

But first, why?

Why would one end up with a JSON array like that?

I think the most common reason is people wanting to imitate tuples, when all their toolbox gives them is dynamically-typed arrays.

This might also come from a slightly silly desire to optimize JSON, to avoid repeating object keys, if there are lots of these little arrays in the JSON data. (Silly because JSON is a very wasteful format to begin with; if you really wanted something efficient, you should be looking elsewhere.)

Custom unmarshal

We can customize how our Notification type is represented in JSON by implementing json.Unmarshaler and/or json.Marshaler, depending on direction. To keep the examples simple, we'll talk only about unmarshaling here.

We could just start by unmarshaling the JSON into an []interface{} or even just a interface{}, and work up from there:

// DON'T DO THIS
func (n *Notification) UnmarshalJSON(buf []byte) error {
	var tmp []interface{}
	if err := json.Unmarshal(buf, &tmp); err != nil {
		return err
	}
	if g, e := len(tmp), 3; g != e {
		return fmt.Errorf("wrong number of fields in Notification: %d != %d", g, e)
	}

	// ... now check every field ...
	msg, ok := tmp[0].(string)
	if !ok {
		return errors.New("Notification first field must be a string")
	}
	n.Message = msg

	prio, ok := tmp[1].(float64)
	if !ok {
		return errors.New("Notification second field must be a number")
	}
	// TODO check that prio is an integer; json numbers are float64s!
	// TODO check that prio is in range for uint8
	n.Priority = uint8(prio)

	crit, ok := tmp[2].(bool)
	if !ok {
		return errors.New("Notification third field must be a boolean")
	}
	n.Critical = crit

	return nil
}

That's a lot of manual, tedious, error checking. Arbitrary types inside interface{} is the enemy. encoding/json already knows how to check JSON against Go types. We can do better than that!

Much like in my previous article, we could use []json.RawMessage and then unmarshal field by field with strong types, manually. But just like earlier, we can use interface{} smarter here too, by giving encoding/json the right types to work with.

Unmarshaling arrays items to fields

What you can do, instead, is prepopulate a []interface{} with pointers to the struct fields. The only remaining check that encoding/json won't do for you is to enforce the length of the tuple.

package main

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

const input = `
["Hello world", 10, false]
`

type Notification struct {
	Message  string
	Priority uint8
	Critical bool
}

func (n *Notification) UnmarshalJSON(buf []byte) error {
	tmp := []interface{}{&n.Message, &n.Priority, &n.Critical}
	wantLen := len(tmp)
	if err := json.Unmarshal(buf, &tmp); err != nil {
		return err
	}
	if g, e := len(tmp), wantLen; g != e {
		return fmt.Errorf("wrong number of fields in Notification: %d != %d", g, e)
	}
	return nil
}

func main() {
	var n Notification
	if err := json.Unmarshal([]byte(input), &n); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", n)
}

Output:

main.Notification{Message:"Hello world", Priority:0xa, Critical:false}

Now, adding or removing fields requires editing just the slice literal assigned to tmp -- the actual unmarshaling logic does not change. If you wanted to, you could use reflect to do even that dynamically, but my personal bar for using reflection is quite a bit higher.