Go言語における標準JSONライブラリ、null周りの振舞い

こんにちは、東芝のデジタルイノベーションテクノロジーセンターの樽家（たるい）と申します。普段はRubyistとして動的言語を愛用しながら、社内の各事業部がサービスを提供する際のプラットフォームを共通機能として社内向けに提供する仕事をしています。

現在このプラットフォームのAPI部分として、実装にGo言語を採用してJSONを用いたRESTfulAPIを使用していますが、この中核となるJSONのライブラリについて標準で添付されているencoding/jsonライブラリ[^1]を使ってみて、動的型付け言語を使っているとあまり意識しないnullの扱いについて色々と興味深い点があったので、ここで紹介したいと思います。

## Interface{}というマジック
Go言語は静的型付けの言語ですが、Interface{}とrefrectを利用したら割となんでもできます。
```go:example1.go
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var x interface{}
    a := []byte(`{"a":["b",null]}`)
    json.Unmarshal(a, &x)
    fmt.Printf("result:%v", x)
}
```
```:result1
result:map[a:[b <nil>]]
```
結果だけみると簡単ですね。しかし、コードは割愛しますが、ここから値を取り出そうとすると型スイッチを駆使するなど、かなり苦労しますし、せっかく静的型付け言語を使っているのにわざわざコストを掛けて何をやっているのかと思うことになるので、あまりお勧めできません。時間がある人は遊び感覚で書いてみると色々な発見があって良いかもしれません。

## 普通の構造体でデコードする
次は普通の構造体を対象に出コードしてみます。

```go:example2.go
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var x struct {
        A []string
    }
    json.Unmarshal([]byte(`{"a":["b",null]}`), &x)
    fmt.Printf("result:%#v\n", x)
    json.Unmarshal([]byte(`{"a":[]}`), &x)
    fmt.Printf("result:%#v\n", x)
    json.Unmarshal([]byte(`{"a":null}`), &x)
    fmt.Printf("result:%#v\n", x)
}
```
```:result2
result:struct { A []string }{A:[]string{"b", ""}}
result:struct { A []string }{A:[]string{}}
result:struct { A []string }{A:[]string(nil)}
```
nullが""になりました。これはGo言語には初期化時この値になるというゼロ値という概念があるために、その値が使われています。nullが””になったというのは実は正確ではなくnullの場合は処理しない[^2] ため、予め別の値が入っているとその値がそのまま使われることになります。この特徴を利用して簡単にデフォルト値を設定するやり方もあるので、気になる人は調べてみてください。
さて、nullを区別する方法はあるのでしょうか？次の方法がその答えになります。

## ポインタ付きの構造体を用いてデコードする
```go:example3.go
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var x struct {
        A []*string
    }
    json.Unmarshal([]byte(`{"a":["b",null,""]}`), &x)
    fmt.Printf("result:%#v\n", x)
}
```
```:result3
result:struct { A []*string }{A:[]*string{(*string)(0xc0000141c0), (*string)(nil), (*string)(0xc0000141e0)}}
```
ちょっと、どんな文字が入っているかはこの表示からはわかりませんが、真ん中にポインタが割り当たってないのだけはわかりますね。一方、3番目の要素は""(空文字列)ですが、ポインタが割り当たっています。このようにポインタを用いると、nullの部分はポインタが指し示す先がないという形でnullを判別できるようになります。

次は逆に構造体から文字列のJSONを取得してみます。
## JSON文字列へのエンコードとomitempty

```go:example4.go
package main

import (
"encoding/json"
"fmt"
)

func main() {
type A struct {
NumberA int
NumberB *int
TextA   string
TextB   *string
}
type B struct {
NumberA int     json:",omitempty"
NumberB *int    json:",omitempty"
TextA   string  json:",omitempty"
TextB   *string json:",omitempty"
}
int0 := 0
y, _ := json.Marshal(A{NumberA: 0, NumberB: &int0})
fmt.Printf("result:%s\n", y)
y, _ = json.Marshal(B{NumberA: 0, NumberB: &int0})
fmt.Printf("result:%s\n", y)
}
```
```:result4
result:{"NumberA":0,"NumberB":0,"TextA":"","TextB":null}
result:{"NumberB":0}
```
ここでは、nullに関係する要素として、構造体のタグに書くomitemptyというアノテーションについて詳しく見ていくことにします。
omitemptyを使うことで、値がゼロ値だった場合に、そのKeyを省略するよう指示することができます。ただし、ゼロ値だった場合というのが問題で、２つ目の結果のように0を指定したつもりでまたは""を指定したつもりで、ゼロ値なのでこれが表示されないという事が発生します。これはかなり致命的になりうるので、omitemptyはポインタと組み合わせて使用する事を強く推奨します。ポインタの場合はポインタの指し示す先があるかないかだけで判断されるため、こういったすっぽ抜けがなくなります。

以上、少し気になった挙動についてまとめてみました。
実際にはJSONのGO言語ライブラリはいくつも出てきているのでその中で目的に合うもの利用するのもいいかなとは思います。しかしながら、どれだけメンテナンスされていくかという面で標準ライブラリを使用する場面も多いのでそういった場面で、参考になれば幸いです。



[^1]: https://pkg.go.dev/encoding/json
[^2]: https://pkg.go.dev/encoding/json#Unmarshaler