every Tech Blog

株式会社エブリーのTech Blogです。

Go 1.24 の encoding/json の omitzero について

この記事は every Tech Blog Advent Calendar 2024 19 日目の記事です。

はじめに

こんにちは、@きょーです!普段はデリッシュキッチン開発部のバックエンド中心で業務をしています。

導入

Go 1.24 が来年の 2 月にリリースされます。 ドラフトではありますが Go 1.24 のリリースノートは既に公開されています(2024 年 12 月 19 日時点)。

tip.golang.org

encoding/jsonomitzeroという json タグが追加されomitemptyと何がどう違うのか気になったため、調べたことについて共有していこうと思います。

encoding/json とは

json のエンコードとデコードを実装しているパッケージになります。json と Go の値の対応付けを Marshall、Unmarshal によって行っています。

pkg.go.dev

サーバーからのレスポンスを json 形式にしてデータを返す際や、リクエストからデータを読み込む際に使うことが多いパッケージかと思います。

まずはomitzeroを理解するためにomitemptyの挙動とその問題点についてみていきます。

omitempty について

Go の構造体を json に変換する際に、フィールドの値がゼロ値の場合にそのフィールドを出力しないという便利な機能です。この機能を活用することで、json のサイズを削減したり、不要な情報を排除したりできます。

以下のように json タグにomitemptyとつければ機能します。

type User struct {
    ID        int       `json:"id,omitempty"`
    Name      string    `json:"name,omitempty"`
}

実際に空の構造体を用意して動かしてみましょう。全てのフィールドがゼロ値となり、何も出力されないことが期待されます。

func main() {
    u := User{}
    jsonData, _ := json.Marshal(u)
    fmt.Println(string(jsonData))
}

output は以下のようになり、省略してほしい値は出力されていないことが確認できました。

{}

しかしomitemptyには 2 つの課題がありました

課題1

プリミティブな値で構成された空の構造体に対して omitempty を指定するとフィールドが出力されてしまう点です。

例を見ていきましょう。

以下のような構造体で、先ほどと同じように実行してみます。

type User struct {
    ID           int          `json:"id,omitempty"`
    Name         string       `json:"name,omitempty"`
    SampleStruct SampleStruct `json:"sample_struct,omitempty"`
}

type SampleStruct struct {
    SampleField1 int `json:"sample_field_1"`
    SampleField2 int `json:"sample_field_2"`
}

output は以下のようになり、omitemptyを指定している構造体の値が空なので何も出力されないことを期待しますが、構造体のフィールド名が出力されてしまっています。

{ "sample_struct": {} }

課題2

課題 1 に繋がるところではありますが、構造体自体はその中身が空であっても omitempty はゼロ値とはみなさない点です。

これも例を見ていきましょう。

以下のような構造体で、先ほどと同じように実行してみます。

type User struct {
    ID        int       `json:"id,omitempty"`
    Name      string    `json:"name,omitempty"`
    CreatedAt time.Time `json:"created_at,omitempty"`
    UpdatedAt time.Time `json:"updated_at,omitempty"`
}

output は以下のようになりました。intstring などのプリミティブな値は期待通り出力されていませんが、構造体を指定した箇所は出力されてしまっています。

{
  "created_at": "0001-01-01T00:00:00Z",
  "updated_at": "0001-01-01T00:00:00Z"
}

これは Go の仕様で以下の値しかゼロ値を定められていないためです。

変数や各要素 ゼロ値
bool false
数値型 0
文字列 ""
ポインタ nil
関数 nil
インターフェース nil
スライス nil
チャンネル nil
マップ nil

tip.golang.org

上記のような悩みを解決できるものがomitzeroになります。

omitzero について

Go 1.24 から使える json タグの一つで、omitemptyと同様にフィールドに指定することで使えるようになります。

先ほどあげた課題が解決されるようになったので、確認していきます。

課題1に対して

プリミティブな値で構成された空の構造体も省略されるようになります。

以下のような構造体で、先ほどと同じように実行してみます。

type User struct {
    ID           int          `json:"id"`
    Name         string       `json:"name"`
    SampleStruct SampleStruct `json:"sample_struct,omitzero"`
}

type SampleStruct struct {
    SampleField1 int `json:"sample_field_1"`
    SampleField2 int `json:"sample_field_2"`
}

output は以下のようになり、期待している値になっていることが確認できました!

{}

課題2に対して

omitzeroオプションが追加されたフィールドの構造体に IsZero メソッド(帰り値は bool)がある場合はそのメソッドを用いてゼロ値かどうかを判定し、ゼロ値である場合はomitemptyと同様にエンコードから省略されるようなります。

以下のような構造体で、先ほどと同じように実行してみます。

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"created_at,omitzero"`
    UpdatedAt time.Time `json:"updated_at,omitzero"`
}

output は以下のようになり、カスタムした値を初期に設定した構造体も省略されていることがわかります。

{}

これは time パッケージの Time 構造体にある IsZero メソッドがゼロ値チェックに使われ、ゼロ値であったために省略されるようになったことを示しています

// IsZero reports whether t represents the zero time instant,
// January 1, year 1, 00:00:00 UTC.
func (t Time) IsZero() bool {
    return t.sec() == 0 && t.nsec() == 0
}

pkg.go.dev

例として Time 構造体を使いましたが、以下のように構造体とそのメソッドである IsZero を用意することで自分たちのアプリケーションに合わせたゼロ値を定義できるようになります。

type User struct {
    ID           int          `json:"id,omitempty"`
    Name         string       `json:"name,omitempty"`
    SampleStruct SampleStruct `json:"sample_struct,omitzero"`
}

type SampleStruct struct {
    SampleField1 int `json:"sample_field_1"`
    SampleField2 int `json:"sample_field_2"`
}

func (s SampleStruct) IsZero() bool { // 何の値をゼロ値とするか決められるようになる
    return s.SampleField1 == 1 && s.SampleField2 == 1
}

func main() {
    u := User{}
    jsonData, _ := json.Marshal(u)
    fmt.Println(string(jsonData))
}

使う際は慎重に

ここまでゼロ値の省略について話してきましたが、使う際は慎重に使っていきましょう(omitempty もそうですが)。

例えばレコードごとの ID や作成日時などはサーバー側では一見不要に見えるかもしれませんが本当に不要かどうかは判断できません。 省略しても良いかどうかはアプリケーションごとのロジック(ゼロ値をそのまま使いたい場合もたくさんある)にもよりますし、フロントやクライアント側でどう使われているかを把握していなければ適切に使うことは難しい気がしています。

まとめ

以上 Go 1.24 でencoding/jsonに追加されるomitzeroオプションの説明でした。軽くまとめます。

  • omitempty
    • ゼロ値の場合、json 出力から省略する
  • omitzero
    • プリミティブな値で構成された空の構造体の場合はフィールド名も含めて省略する
    • 構造体にIsZeroメソッドが実装されている場合、そのメソッドの結果に基づいてゼロ値かどうかを判断し、ゼロ値であれば省略する

どちらを使うべきか

  • 構造体のゼロ値判定が必要な場合
    • omitzero
  • 構造体以外のゼロ値を判定したい場合
    • omitempty

注意点

  • 必須フィールドやビジネスロジック上重要なフィールドを誤って省略しないように注意が必要

最後に

Go 1.24 楽しみですね! 今回紹介したものはほんの一部ですので、気になった方はぜひリリースノート追ってみてください!

明日のアドベントカレンダーは 庄司(ktanonymous)さんによる 「エブリー初のエンジニア向け内定者研修を実施しています」 です!

参考

PS

みなさんは好きなゲームありますか?僕は最近 DAVE THE DIVER というゲームにハマっているのですが、永遠に時間が溶けててやばいです。ゲームのやりすぎで朝日を見る事も厭わない勇気有る方達、どうぞ一歩前へ。

store.steampowered.com