GoでJSONのnullをいい感じに扱いたいことがあるとします。しかしGoではnullを扱うのは容易ではありません。
Goにはnilが存在しますが、これはポインタ型でしか使えません。よってintやstring型では使用できません。Goはintやstring型は初期化しなかった場合、ゼロ値に初期化されます。intのゼロ値は0、stringのゼロ値は空文字列です。そのためJSONのnullをGoで扱おうとした場合、Goのゼロ値との区別ができません。
同じ問題はSQLでもあります。nullが存在するカラムから値を取得した際にnullとGoのゼロ値を区別する必要があります。そこでGoのdatabase/sqlではNullStringのようなstructが定義されています。
type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}
Validの真偽値を確認することで空文字列とnullを区別することができます。これと同じことをJSONでやるにはどうしたらよいでしょうか。
json.Marshalとjson.Unmarshalの動きを変更するには以下のインタフェースを実装します。
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
そこで以下のような実装をしてみました。
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type NullSegments struct {
Segments Segments
Valid bool // Valid is true if Segments is not NULL
}
type Range struct {
From int `json:"from"`
To int `json:"to"`
}
type Segments struct {
Status int `json:"status"`
Range Range `json:"range"`
}
type Settings struct {
Segments NullSegments `json:"segments"`
}
var nullLiteral = []byte("null")
func (s *NullSegments) UnmarshalJSON(b []byte) error {
if bytes.Equal(b, nullLiteral) {
return nil
}
err := json.Unmarshal(b, &s.Segments)
if err == nil {
s.Valid = true
return nil
}
return err
}
func (s NullSegments) MarshalJSON() ([]byte, error) {
if s.Valid {
return json.Marshal(s.Segments)
} else {
return nullLiteral, nil
}
}
func main() {
b := []byte(`{"segments": {"status": 1, "range": {"from": 10, "to": 20}}}`)
// b := []byte(`{"segments": null }`)
s := &Settings{}
json.Unmarshal(b, s)
bb, _ := json.Marshal(s)
fmt.Println(string(bb))
}
注意点としては[]byteをstringにキャストするのはコストが高いのでbytes.Equalなどのメソッドを使います。
bool型のゼロ値はfalseなのでnullが来たときは何もしなくて大丈夫です。null以外が来た場合にValidをtrueにする必要があります。