年中アイス

いろいろつらつら

GoでJSONの数字/数値を扱う

JSONはデータの表現形式です。表現はできますが、項目があるかや、型が何であるかを検査する仕組みはありません。*1

そのため、システムや実装が分かれている場合に、数値を期待しているが、JSONの表現上、数値か数字かがずれていることが起き得ます。どういうことかというと、"numeric":1234 (数値)なのか、"numeric":"1234" (数字)なのかが違う、または不定ということ。仕様の齟齬や、実装ミスなどで発生します。

動的型付け言語の場合、よしなに扱ってくれたりしますが、Goにそれをやられると型エラーが発生してしまいます。GoでJSONを読み込む場合、encoding/jsonパッケージjson.Unmarshal()を使って、structに読み込む方法があります。しかし、stringで定義していたら、数値がNG、int系で定義していたら、数字がNGです。

stringは数値がダメ(playground)

type Num struct {
    Number string
}

num := Num{}
err := json.Unmarshal([]byte(`{"number":1234}`), &num)
    
// json: cannot unmarshal number into Go struct field Num.Number of type string

int64は数字がダメ(playground)

type Num struct {
    Number int64
}

num := Num{}
err := json.Unmarshal([]byte(`{"number":"1234"}`), &num)

// json: cannot unmarshal string into Go struct field Num.Number of type int64  

Goの場合、Unmarshalで読み込めないと、黒魔術的にJSONを解析する必要がある*2ので、Unmarshalで済ませたいのです。その際に、json.Numberを使います。

使い方は簡単で、数値数字不定になっている属性の型をjson.Numberにして、そのstructを使ってUnmarshalします。そうすると数字でも数値でもabcdeなどの文字列でも、Unmarshal時点ではエラーになりません。型の不一致で、JSON全体の読み込みを止めなくて済みます。もちろん使う時に数値変換は必要で、Int64()を使いますが、数字以外の文字列の場合はエラーになります。(ParseInt失敗するのと同様)

type Num struct {
    Number1 json.Number
    Number2 json.Number
    Number3 json.Number
}

num := Num{}
// Unmarshalではエラーにならない
err := json.Unmarshal([]byte(`{"number1":1111,"number2":"2222","number3":"abcde"}`), &num)
// 数値はエラーにならない
num1, err := num.Number1.Int64()

// 数字はエラーにならない
num2, err := num.Number2.Int64()
    
// 数字以外の文字はエラー
// strconv.ParseInt: parsing "abcde": invalid syntax
_, err = num.Number3.Int64()

json.Number使ったUnmarshal(playground)

補足:JSON出力する時のjson.Number

json.Numberは、Marshalすると、数値として出力されます。数字、数値どちらで読み込んでいても数値になります。数以外の文字列の場合は、Marshal時にエラーになります。このjson.Numberを使って読み込んで、そのままMarshalする場合は、この数値になる動きを意識しておく必要があります。

json.NumberのMarshal

type Num struct {
    Number1 json.Number `json:"number1"`
    Number2 json.Number `json:"number2"`
}

num := Num{}
// 数値、数字を読み込む
err := json.Unmarshal([]byte(`{"number1":1111,"number2":"2222"}`), &num)

// JSONに出力
j, err := json.Marshal(num)

// 数値として出力される
// {"number1":1111,"number2":2222}
fmt.Println(string(j))

*1:JSON Schemaなど別の仕組みを使用するとチェックしたりできるようです

*2:map[string]interface{}で延々名前探して型確認して変換の繰り返し。。。