Help us understand the problem. What is going on with this article?

Goのjson.Unmarshalで値にJSONのnumber、string両方の可能性がある場合

More than 3 years have passed since last update.

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)
    }
}

結果。Go Playgroundで実行

unmarshal success 100
unmarshal success 200

独自の型を定義して便利に

json.Numberの場合、内部はstringのため数値として扱うには毎回変換が必要で少し面倒です。golang.org/x/oauth2で採っている方法のように、独自の型を定義して扱うのが良さそうです(参考: internal/tokens.go#L59-L90expirationTime)。このライブラリでは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
}

Go Playgroundで実行

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away