Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

json.Unmarshalでmap[string]interface{}型にパースするときの注意点

サーバーサイドエンジニアの田実です!

今回はGoの json.Unmarshal 関数で map[string]interface{} の型を指定したときに発生していた事象とその対策を併せて紹介します!

なにが起きていたか

以下のように任意のキー、バリューを含むJSON文字列を map[string]interface{} 型でパースして、任意のバリューを文字列として取り出す処理がありました。

var res map[string]interface{}
json.Unmarshal([]byte(`{"test": 1234567}`), &res)
fmt.Printf("%v", res["test"])

任意のバリューがstring, bool、小さいintの値ではうまくいったのですが、上記のように大きいintを含むJSONをパースしたときに、指数表記で文字列が表示されました(本当は 1234567 という文字列がほしい

1.234567e+06

なぜ発生したか

どうやら、json.Unmarshalでinterface{}を指定すると数字はfloat64でパースされ、大きいfloat64を "%v" で表示すると指数表記で表示されるようです。 stackoverflow.com

確かにコードを読むと数値系はfloat64にパースされています。

func (d *decodeState) convertNumber(s string) (interface{}, error) {
    // ...
    f, err := strconv.ParseFloat(s, 64)
    if err != nil {
        return nil, &UnmarshalTypeError{Value: "number " + s, Type: reflect.TypeOf(0.0), Offset: int64(d.off)}
    }
    return f, nil
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/encoding/json/decode.go;l=847-852

対策

json.Unmarshalではなくjson.Decoderとdecoder.UseNumber()を使うと、json.NumberというstringのDefined typeでパースされます。

decoder := json.NewDecoder(bytes.NewReader([]byte(`{"test": 1234567}`)))
decoder.UseNumber()
err := decoder.Decode(&res);

コードを読むと、convertNumberの関数の先頭の処理に、useNumberが設定されているとjson.Numberで返す処理が入っています。

// convertNumber converts the number literal s to a float64 or a Number
// depending on the setting of d.useNumber.
func (d *decodeState) convertNumber(s string) (interface{}, error) {
    if d.useNumber {
        return Number(s), nil
    }
    // ...
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/encoding/json/decode.go;l=844-846

このjson.Numberを "%v" で表示すると数値がそのまま文字列で表示されるので、これで対応できました!