In this post, I’m going to explain how to parse JSON data with a field which can be either a string or number in Go.

Go has a great support for JSON. In many cases what you have to do is to define corresponding structs to parse JSON.

Let’s say we’re dealing with a Web API provided by some other company. It gives us a series of name-value pairs in this format:

[
  {
    "name" : "foo",
    "value" : 100
  },
  {
    "name" : "bar",
    "value" : 100
  }
]

We can assume name is a string and value is a number. Thus, the corresponding struct will be like this:

type NameValue struct{
	Name  string
	Value float64
}

Now we can parse the JSON like this:

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	data := `[
		{
			"name" : "foo",
			"value" : 100
		},
		{
			"name" : "bar",
			"value" : 200
		}
	]`

	var nvs []NameValue
	if err := json.Unmarshal([]byte(data), &nvs); err != nil {
		panic(err)
	}

	for i := 0; i < len(nvs); i++ {
		fmt.Printf("result[%d]: %+v\n", i, nvs[i])
	}
}

type NameValue struct{
	Name  string
	Value float64
}
result[0]: {Name:foo Value:100}
result[1]: {Name:bar Value:200}

Cool. Then suddenly you realize that value isn’t always a number- it can be a string:

[
  {
    "name" : "foo",
    "value" : 100
  },
  {
    "name" : "bar",
    "value" : 200
  },
  {
    "name" : "baz",
    "value" : "X300"
  }
]

panic: json: cannot unmarshal string into Go value of type float64

goroutine 1 [running]:
panic(0x124740, 0x10532340)
	/usr/local/go/src/runtime/panic.go:500 +0x720
main.main()
	/tmp/sandbox440216281/main.go:27 +0x140

Now it’s messed up. But no worries. We can handle this situation by making Value either string or number instead of just string:

package main

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

func main() {
	data := `[
		{
			"name" : "foo",
			"value" : 100
		},
		{
			"name" : "bar",
			"value" : 200
		},
		{
			"name" : "baz",
			"value" : "X300"
		}
	]`

	var nvs []NameValue
	if err := json.Unmarshal([]byte(data), &nvs); err != nil {
		panic(err)
	}

	for i := 0; i < len(nvs); i++ {
		fmt.Printf("result[%d]: %+v\n", i, nvs[i])
	}
}

type NameValue struct{
	Name  string
	Value StringOrNumber
}

type StringOrNumber struct{
	String string
}

func (s *StringOrNumber) UnmarshalJSON(data []byte) error {
	dec := json.NewDecoder(bytes.NewReader(data))
	
	var v interface{}
	if err := dec.Decode(&v); err != nil {
		return err
	}
	
	switch v.(type) {
	case string:
		s.String = v.(string)
	case float64:
		s.String = fmt.Sprintf("%d", int(v.(float64)))
	default:
		return fmt.Errorf("unknown type: %+v", v)
	}
	
	return nil
}
result[0]: {Name:foo Value:{String:100}}
result[1]: {Name:bar Value:{String:200}}
result[2]: {Name:baz Value:{String:X300}}

Now we can handle a string-or-number JSON field.

This is based on my real life experience and now I’m seeing a case where Value can also be an object which I don’t know how to handle gracefully. Life is tough.