json.UnmarshalでJSONをデコードする際に、特定のキーの値がJSON number、string両方で来る可能性がある場合に困ったのでその対処方法です。json.Numberを使うことで両方に対応できるようです。
具体的には以下の2つのようなJSONの両方のデコードが必要になる場合です。
{"n" : 100} # JSON numberで来る場合
{"n" : "100"} # JSON stringで来る場合
以下のように単純にstructのintフィールドを使った場合、JSON stringで値が来る場合にエラーになります。
type Foo struct {
N int
}
var foo Foo
// nの値が文字列
data := []byte(`{"n":"100"}`)
if err := json.Unmarshal(data, &foo); err != nil {
// エラーになる
log.Fatalf("unmrshal failed: %v", err)
} else {
fmt.Println("unmarshal success ", foo.N)
}
下記のように変換に失敗します。Go Playgroundで実行
2009/11/10 23:00:00 unmrshal failed: json: cannot unmarshal string into Go value of type int
stringオプションではダメ
下記のようにtagにstring
オプションを指定することで、文字列からintに変換することはできますが、この場合、JSON numberが来るとエラーになります。Go Playgroundで実行
type Foo struct {
N int `json:",string"`
}
参考: https://golang.org/pkg/encoding/json/#Marshal
json.Numberを使う
json.NumberというJSON numberを表現する型を使うことで、JSON number, string両方に対応することができます。内部的にはstringなので、Int64()
, Float64()
などのメソッドを使って必要に応じて型を変換する必要があります。
func main() {
// JSON string
unmarshalWithNumber(`{"n": "100"}`)
// JSON number
unmarshalWithNumber(`{"n": 200}`)
}
func unmarshalWithNumber(data string) {
type Foo struct {
// 型をjson.Number型にする
N json.Number
}
var foo Foo
if err := json.Unmarshal([]byte(data), &foo); err != nil {
log.Fatalf("unmrshal failed: %v", err)
} else {
fmt.Println("unmarshal success", foo.N)
}
}
unmarshal success 100
unmarshal success 200
独自の型を定義して便利に
json.Numberの場合、内部はstringのため数値として扱うには毎回変換が必要で少し面倒です。golang.org/x/oauth2で採っている方法のように、独自の型を定義して扱うのが良さそうです(参考: internal/tokens.go#L59-L90のexpirationTime
)。このライブラリではOAuth2のService Providerの一部がnumberでなくstringで返す仕様の対応に使っています。
func unmarshalWithNumber(data string) {
type Foo struct {
// 独自の型
N MyNumber
}
var foo Foo
if err := json.Unmarshal([]byte(data), &foo); err != nil {
log.Fatalf("unmrshal failed: %v", err)
} else {
fmt.Println("unmarshal success", foo.N)
}
}
type MyNumber int64
// UnmarshalJSONを実装して、内部のint64に変換しておく
func (m *MyNumber) UnmarshalJSON(b []byte) error {
var number json.Number
if err := json.Unmarshal(b, &number); err != nil {
return err
}
i, err := number.Int64()
if err != nil {
return err
}
*m = MyNumber(i)
return nil
}