Edited at

GoでJSONのnullをいい感じに扱いたい

More than 1 year has passed since last update.

GoでJSONのnullをいい感じに扱いたいことがあるとします。しかしGoではnullを扱うのは容易ではありません。

Goにはnilが存在しますが、これはポインタ型でしか使えません。よってintstring型では使用できません。Goはintstring型は初期化しなかった場合、ゼロ値に初期化されます。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.Marshaljson.Unmarshalの動きを変更するには以下のインタフェースを実装します。


interface.go

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))
}

注意点としては[]bytestringにキャストするのはコストが高いのでbytes.Equalなどのメソッドを使います。

bool型のゼロ値はfalseなのでnullが来たときは何もしなくて大丈夫です。null以外が来た場合にValidをtrueにする必要があります。