🆚

【Go】omitempty vs omitzero

に公開

はじめに

Go 1.24から、encoding/jsonomitzeroというタグが追加されました。
いろいろな記事を読んでomitemptyとの違いがなんとなくしか理解できていなかったので、簡単な例を通して理解を深めつつ、調べたことを備忘録的に書いていこうと思います。

この記事でわかること

  • omitemptyで起こること
  • omitzeroの使い方

omitemptyについて

omitemptyタグについて、公式ドキュメントでは以下のように説明されています。

// 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 array,
// slice, map, or string of length zero.

これをざっくり訳すと、omitemptyではフィールドの値がゼロ値だった場合、エンコーディング対象から除外する、と書かれています。
Goにおける ゼロ値とは、「false」「0」「空文字」「nil(スライスやポインタなど)」 を指します。

それでは、omitemptyタグを用いた場合、どのような挙動になるのか見ていきましょう。

main.go
type User struct {
    Name      string    `json:"name,omitempty"`
    NickName  string    `json:"nick_name,omitempty"`
    Age       int       `json:"age,omitempty"`
    Address   Address   `json:"address,omitempty"`
    Hobbies   []Hobby   `json:"hobbies,omitempty"`
    CreatedAt time.Time `json:"created_at,omitempty"`
}

type Hobby struct {
    Name string `json:"name,omitempty"`
}

type Address struct {
    Prefecture string `json:"prefecture,omitempty"`
}

func main() {
    user := User{
        Name: "Taro",
        Age:  20,
    }
    jsonData, _ := json.Marshal(user)
    fmt.Println("jsonData: ", string(jsonData))
}

今回は、User構造体のフィールドのうち、NameAgeのみ値が入っております。
それ以外が特に指定していないので、ゼロ値が入ることになります。

実行すると、以下のような出力結果を得ることができます。

ターミナル
# go run main.go

jsonData: {"name":"Taro","age":20,"address":{},"created_at":"0001-01-01T00:00:00Z"}

この結果は、以下の3点にまとめることができます。

  1. NickNameHobbiesは省略されている
  2. Addressはフィールド名と空構造体が出力されている
  3. CreatedAtには「0001-01-01T00:00:00Z」が入っている

omitemptyタグでは フィールドが空の構造体は「フィールド名と空構造体」 が、time.Time(これも構造体)は「0001-01-01T00:00:00Z」 が出力されてしまうので、ここはomitzeroを使う必要があります。

omitzeroについて

omitzeroタグについて、公式ドキュメントでは以下のように説明されています。

When marshaling, a struct field with the new omitzero option in the struct field tag will be omitted if its value is zero.
(中略)
In particular, unlike omitempty, omitzero omits zero-valued time.Time values, which is a common source of friction.

これをざっくり訳すと、「omitzeroが指定されている場合、その値がゼロであればフィールドは省略される」ことが書かれています。
これだけだとomitemptyと何が違うのか?となりますが、続けて「omitemptyとは異なり、time.Time型のゼロ値を省略する」と書かれており、これがomitemptyとの違いです。

それでは、omitzeroタグをを用いた場合、どのような挙動になるのか見ていきましょう。

main.go
type User struct {
    Name      string    `json:"name,omitzero"`
    NickName  string    `json:"nick_name,omitzero"`
    Age       int       `json:"age,omitzero"`
    Address   Address   `json:"address,omitzero"`
    Hobbies   []Hobby   `json:"hobbies,omitzero"`
    CreatedAt time.Time `json:"created_at,omitzero"`
}

type Hobby struct {
    Name string `json:"name,omitzero"`
}

type Address struct {
    Prefecture string `json:"prefecture,omitzero"`
}

func main() {
user := User{
    Name: "Taro",
    Age:  20,
}
jsonData, _ := json.Marshal(user)
fmt.Println("jsonData: ", string(jsonData))
}

これは先ほどの実装のうち、omitemptyomitzeroに変更しただけです。
これを実行すると、以下のような出力結果を得ることができます。

ターミナル
# go run main.go

jsonData:  {"name":"Taro","age":20}

先ほどは出力されていたAddressCreatedAtも、omitzeroタグをつけたことで省略されました。

まとめ

omitemptyomitzeroはどちらも「値が空の場合にJSON出力から省略したい」ときに使うタグですが、適用範囲と挙動に明確な違いがあります。

omitempty

  • フィールドがゼロ値の場合に省略する
  • ただし構造体はゼロ値でも省略されない
  • 空構造体 {} として出力される
  • time.Timeのゼロ値(0001-01-01T00:00:00Z)もそのまま出力される

つまり、omitemptyは「プリミティブ型やスライスに対しては便利だが、構造体やtime.Timeでは使いづらい」場合があるタグと言えます。

omitzero

  • フィールドの値がゼロ値なら必ず省略する
  • 特にomitemptyが苦手としていた構造体やtime.Timeのゼロ値も省略
  • 「値が入っていないフィールドはJSONから完全に消したい」場面で有効

Go 1.24 以降の新しいタグで、実運用上のニーズに合わせて改善された仕様といえます。

参考

Discussion