LoginSignup
53
37

More than 5 years have passed since last update.

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

Posted at

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で実行

53
37
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
37