Go

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

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