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.