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にする必要があります。