英語614435 views
高校日本史190647 views
雑学1473736 views
ヒストリア291495 views
りんご212052 views
数学講師2891552 views
LaTeX962830 views
中学英語812106 views
高校倫理1441073 views
中学理科1631340 views
Help
Tools

English

Go で JSON のフィールドを省略する:omitempty と omitzero の使い分け

構造体を JSON に変換するとき、値がないフィールドを出力から省きたいケースは多いです。omitempty を使えばゼロ値のフィールドを省略できますが、その挙動には意外な落とし穴があります。Go 1.24 で追加された omitzero との違いも押さえておきましょう。

omitempty の基本

構造体タグに omitempty を付けると、フィールドがゼロ値のとき JSON 出力から省略されます。

type User struct {
	Name    string `json:"name"`
	Email   string `json:"email,omitempty"`
	Age     int    `json:"age,omitempty"`
	IsAdmin bool   `json:"is_admin,omitempty"`
}

func main() {
	user := User{Name: "Alice"}
	data, _ := json.Marshal(user)
	fmt.Println(string(data))
	// {"name":"Alice"}
}

Email(空文字列)、Age(0)、IsAdmin(false)はすべてゼロ値なので出力されません。Name には omitempty が付いていないため、空文字列でも出力に含まれます。

各型のゼロ値は以下のとおりです。

string""(空文字列)
int / float0
boolfalse
ポインタnil
スライスnil(空スライスは省略されない)
マップnil(空マップは省略されない)

omitempty の落とし穴

omitempty が問題になるのは、ゼロ値に意味がある場合です。

type Product struct {
	Name  string `json:"name"`
	Price int    `json:"price,omitempty"`
	Stock int    `json:"stock,omitempty"`
}

func main() {
	product := Product{Name: "無料サンプル", Price: 0, Stock: 0}
	data, _ := json.Marshal(product)
	fmt.Println(string(data))
	// {"name":"無料サンプル"}
}

Price が 0 円の商品も、Stock が 0 個の商品も、その情報をクライアントに返したいはずです。しかし omitempty は「ゼロ値かどうか」だけで判断するため、意味のあるゼロも未設定のゼロも区別できません。

古典的な対処法はポインタ型を使うことです。

type Product struct {
	Name  string `json:"name"`
	Price *int   `json:"price,omitempty"`
	Stock *int   `json:"stock,omitempty"`
}

func main() {
	price := 0
	stock := 0
	product := Product{Name: "無料サンプル", Price: &price, Stock: &stock}
	data, _ := json.Marshal(product)
	fmt.Println(string(data))
	// {"name":"無料サンプル","price":0,"stock":0}
}

ポインタの場合、ゼロ値は nil(値が設定されていない状態)です。0 を指すポインタは nil ではないので省略されません。ただしポインタを多用するとコードが複雑になりがちで、nil チェックの手間も増えます。

omitzero(Go 1.24 以降)

Go 1.24 で導入された omitzeroomitempty の問題点を解消するために設計されました。

omitempty

型ごとに固定されたゼロ値で判定する。int の 0、bool の false、構造体の空値がすべて省略対象になる。

omitzero

型が IsZero() メソッドを持っていればそれを呼んで判定する。より意味のある「空かどうか」の判断が可能。

omitzero がもっとも威力を発揮するのは time.Time の扱いです。

type Event struct {
	Name      string    `json:"name"`
	StartedAt time.Time `json:"started_at,omitempty"`
	EndedAt   time.Time `json:"ended_at,omitzero"`
}

omitempty だと time.Time のゼロ値判定が曖昧で、空の時刻が "0001-01-01T00:00:00Z" として出力されてしまうことがあります。omitzero なら time.TimeIsZero() メソッドが呼ばれるため、ゼロ時刻は確実に省略されます。

自作の型で omitzero を使う

IsZero() bool メソッドを実装すれば、独自の型でも omitzero の恩恵を受けられます。

type OptionalInt struct {
	Value int
	Set   bool
}

func (o OptionalInt) IsZero() bool {
	return !o.Set
}

func (o OptionalInt) MarshalJSON() ([]byte, error) {
	return json.Marshal(o.Value)
}
type Config struct {
	Name     string      `json:"name"`
	MaxRetry OptionalInt `json:"max_retry,omitzero"`
}

func main() {
	// Set が false → IsZero() が true → 省略される
	c1 := Config{Name: "default"}
	data1, _ := json.Marshal(c1)
	fmt.Println(string(data1))
	// {"name":"default"}

	// Value が 0 でも Set が true → 出力される
	c2 := Config{Name: "custom", MaxRetry: OptionalInt{Value: 0, Set: true}}
	data2, _ := json.Marshal(c2)
	fmt.Println(string(data2))
	// {"name":"custom","max_retry":0}
}

ポインタを使わなくても「未設定」と「値が 0」を区別できるようになります。

実務での使い分け

文字列・スライスの空チェックomitempty で十分
int や bool のゼロに意味があるomitzero またはポインタ
time.Time の省略omitzero が最適
カスタム型の省略制御omitzero + IsZero() を実装
Go 1.23 以前のプロジェクトomitempty + ポインタで対応

既存コードを一斉に書き換える必要はありません。新しく書くコードで数値や時刻を扱う場面では omitzero を選び、文字列やスライスの単純な空チェックには omitempty を使い続ける。この組み合わせが現実的でしょう。