Overview
Go 1.24 から encoding/json に入った IsZero
のインターフェースについて実際に触ってみて挙動を調べてみました。
IsZero とは?
When marshaling, a struct field with the new omitzero option in the struct field tag will be omitted if its value is zero. If the field type has an IsZero() bool method, that will be used to determine whether the value is zero. Otherwise, the value is zero if it is the zero value for its type. The omitzero field tag is clearer and less error-prone than omitempty when the intent is to omit zero values. In particular, unlike omitempty, omitzero omits zero-valued time.Time values, which is a common source of friction. If both omitempty and omitzero are specified, the field will be omitted if the value is either empty or zero (or both).
リリースノートを参考にすると今まで time.Time
に入っていた IsZero
メソッドを生やしたカスタムフィールドに対して json タグで omitzero
を付与することでいわゆる「ゼロ値」の振る舞いを json.Marshal, Unmarshal の中で制御することができるようになるようです。
サンプル実装
まず標準の time.Time
を渡してみます。
// You can edit this code! // Click here and start typing. package main import ( "encoding/json" "fmt" "time" ) type X struct { A time.Time `json:"a,omitzero"` } func main() { x := X{} b, _ := json.Marshal(x) fmt.Printf("%v", string(b)) }
ref: https://go.dev/play/p/nkBKMnKym7i
この出力以下のようになります。
{}
続いてカスタム型に IsZero
メソッドを生やしてみます。
// You can edit this code! // Click here and start typing. package main import ( "encoding/json" "fmt" "time" ) type X struct { A time.Time `json:"a,omitzero"` B Y `json:"b,omitzero"` } type Y struct { UserID int } func (y Y) IsZero() bool { fmt.Println("aaaa") return true } func main() { x := X{} b, _ := json.Marshal(x) fmt.Printf("%v", string(b)) }
ref: https://go.dev/play/p/sDVeZEzEZlm
この出力は以下のようになります。
aaaa {}
これは IsZero
メソッドの返り値が true (= つまりどんな値をとっても omitzero で消える) なので出力結果は空になります。
ただ少しコードを変更してみます。
// You can edit this code! // Click here and start typing. package main import ( "encoding/json" "fmt" "time" ) type X struct { A time.Time `json:"a,omitzero"` B Y `json:"b,omitzero"` } type Y struct { UserID int } func (y Y) IsZero() bool { return y.UserID == 0 } func main() { x := X{ B: Y{ UserID: 1, }, } b, _ := json.Marshal(x) fmt.Printf("%v", string(b)) }
ref: https://go.dev/play/p/roo_uNbSGKI
この出力は以下のようになります。
{"b":{"UserID":1}}
Y の中にある UserID が 0 のときは omitzero で消えるようにしてるので、0 以外を指定したときには omitzero の対象になりません。
振る舞いとユースケース
この omitzero (IsZero メソッド)の導入により、今まで json の key から消すには json で Unmarshal するフィールドの定義を参照型にする必要があり、これが意図しない nilpo の温床になっていたり、クライアントアプリから見るとあるはずの key がない、と言ってフロントのエンバグに繋がることもありましたが、今回 の omitzero の導入で「どういうときにゼロとするか?」という仕様を決めることができるようになると思いました。Go の実装としても不要な参照型を排除することで nilpo を踏む可能性を少なくできますし、より堅牢なプログラムを書けるようになりそうです。
また別のユースケースとしては、A/B テストや本番と開発環境(テスト環境等)で特定のフィールドを入れる、入れないを IsZero の中で制御し、クライアントの振る舞いを変更するといったことも実装の深いところではなく、JSON を作るより上層(Converter や View といったレイヤー)で行うことができて不要な依存を深いところに埋め込まなくて済む、といったことも考えられそうです。
まとめ
最初は今更 encoding/json におけるゼロ値の取り扱いを再定義することのメリットは何なのか、ぱっとわかりませんでしたが、実際に実装してみると少しわかりました。使っていきたいと思います。