はじめに
こんにちは。ペンギンになりたいエンジニアの島ノ江です。普段は CSIG で「FutureVuls」という脆弱性管理サービスの開発・営業を担当しています。
Go1.24リリース連載の3日目。Go 1.24 で encoding/json に追加される omitzero
を扱います(リリースノート)
omitzero とは
omitzero
は、Go の構造体を JSON に変換する際、フィールドの値がゼロ値である場合にそのフィールドを省略してくれるタグオプションです。json 変換の歯がゆい部分を修正してくれるアップデートです。
これまで Go にはフィールドが空の値のときに json 出力から省略する omitempty
というタグがありました。omitempty
では、数値型の 0
、文字列型の ""
(空文字列)、ポインタ・スライス・マップのnil
が json フィールドに含まれる場合、その値が省略されます。
しかしこの omitempty
、struct(特に time.Time{}
型)については省略されず、APIレスポンスに不要なフィールドが含まれてしまうなどの問題がありました。
実際のコードで見てみる
playground を使って omitempty
, omitzero
の挙動を見ていきます。
struct の json タグに omitempty
, omitzero
の両方を指定して、 Go1.23 と Go dev branch の両方で実行してみます。なお、omitempty
と omitzero
を同時に指定すると、値が空かゼロの場合に省略されます。
If both omitempty and omitzero are specified, the field will be omitted if the value is either empty or zero (or both).
https://go.dev/play/p/s_Z_tsX5s7k
type Pet struct { |
Go 1.23 までの挙動
Go 1.23 のブランチで実行すると、以下のような出力結果になります。 Bob の結果を見ると
- struct の
Pet
が省略されず、各フィールドがゼロ値の struct になる time.Time
型のフィールドがゼロ値の"0001-01-01T00:00:00Z"
になる
{"Age":25,"Name":"Alice","Hobby":"Game","Pet":{"Name":"Ace","Age":5},"CreatedAt":"2025-01-23T04:56:07Z"} |
Bob では Hobby
, Pet
, CreatedAt
の3つのフィールドを指定していないので、気持ちとしてはこれらも出力からは省略されてほしいですが、不要な値が含まれています。
omitempty
では time.Time{}
のゼロ値が省略されないことを回避するために、あえてポインタ型を使う等の策がとられていました。
Go 1.24 での挙動
omitzero
が追加された Go dev branch で実行すると、以下のような出力結果になります。
期待通り、宣言時に入力しなかった Pet
, CreatedAt
のフィールドも出力から省略されています。不要な出力が含まれておらずこれはうれしい。
{"Age":25,"Name":"Alice","Hobby":"Game","Pet":{"Name":"Ace","Age":5},"CreatedAt":"2025-01-23T04:56:07Z"} |
その他
struct や time.Time{}
の扱い以外に omitzero
の特徴を見ていきます。
IsZero()
のカスタマイズ
json の field
には IsZero() bool
のインターフェースが定義されており、このメソッドを実装することでゼロ値の定義を柔軟に変更できるようになります(実装)
IsZero() bool
メソッドが存在する場合は、このメソッドの戻り値の true/false- そうでない場合は、
reflect.Value.IsZero
に従ってそのフィールドがゼロ値であるか
により、フィールドを省略するかどうかを判定するようになります。
例えば、以下のように「Pet の年齢が0以下であればゼロ値とみなす」という定義をすると、ゼロ値の定義が変わり、IsZero() bool
が条件を満たすときに Pet
のフィールドも省略されています。
https://go.dev/play/p/kuhfd4mDw2I?v=gotip
func (p Pet) IsZero() bool { |
空とnilの区別
スライスにおいて、omitempty
では空スライスとnilスライスはどちらも省略されてしまいます。一方で、omitzero
ではこれらを区別するようになり、「リストが存在しないのか」「リストの要素がないのか」を分けることができるようになります。これはマップについても同様です。
https://go.dev/play/p/xwOXmnAEfVe?v=gotip
func printResponse(res Response) { |
本機能の歴史
このタグが追加された Issue を見てみると、起源は 2021年4月21日まで遡ります。
https://github.com/golang/go/issues/45669
omitempty
が指定された際に省略されるケースのうち、空の struct に関する記述がドキュメントにないというのが発端のようです。空の struct についても、数値型や文字列型と同様に省略されるのが自然だという問題提議です。
The “omitempty” option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.
これについてはコミュニティでも認識されていましたが、具体的な解決策にいたらずという形で課題が残っていました(コメント)。encoding/json
にはバグともとれるような問題があるが広く使われているものであり、後方互換性や現在並行して進めている encoding/json/v2 も考慮しなければならず、慎重に議論されてきたようです。それが遂に Go1.24 で終着しました。
さいごに
今後は、 time.Time{}
型なども含めてフィールドがゼロ値である場合に省略してくれる omitzero
を基本的に利用していくのが、明快さ・柔軟性ともに良いと思われます。後方互換性も考慮して omitempty
は引き続き使えますが、それ以外にあえて omitempty
を選択する理由はなさそうです。
実際私も初めて Go の API 周りに触れた際、time.Time{}
を marshal した際に omit されず「??」となりました。 Go は時間のゼロ値も(一般的な時間のゼロ値である)UNIX時間と異なるなど、初学者が徒に混乱してしまう仕様があります。アップデート毎に、少しずつこのような歯がゆい部分が改善されていくのは良いですね。
以上で本ブログを終えます。Go連載では初めての執筆でしたが、Issue やリリースノートを読んでいくと言語の理解も深まり、学びが多くて面白かったです。
それでは、またの機会で。