Golang JSON `omitempty`: Zero Values, `omitzero`, and Nested Structs

Deepak Prasad
Tested on RHEL 10.2 with Go 1.27.0
Package go 1.27.0
Applies to Any host with Go 1.20+ installed (omitzero requires Go 1.24+; encoding/json/v2 section requires Go 1.27+)
Privilege Normal user
Scope encoding/json omitempty behavior, pointers, slices, nested structs, Go 1.24+ omitzero, and Go 1.27 json/v2 semantic differences. Does not cover full JSON parsing, streaming decoders, or a complete json/v2 migration.
Related guides Parse JSON in Go
json.Unmarshal in Go
Structs in Go
Golang tutorial
Getting started with Go

In encoding/json, json:"field,omitempty" omits a field during marshaling when its value is considered empty under the v1 rules. Go 1.24 added omitzero, and Go 1.27 introduced stable encoding/json/v2, where omitempty uses different semantics. This guide covers the widely used encoding/json behavior first, then omitzero and the v2 change—so you do not treat one rule as universal across packages.


What omitempty means in encoding/json

encoding/json applies omitempty only when you marshal Go values to JSON. The tag has no effect on unmarshaling.

For the established v1 API, a field is omitted when its Go value is empty:

Go value omitempty in encoding/json
"" omitted
0 omitted
false omitted
nil pointer omitted
nil interface omitted
nil or zero-length slice ([]T{}) omitted
nil or zero-length map omitted
[0]T{} / zero-length array omitted
zero struct value not omitted

That last row is the famous nested-struct surprise: a zero struct is still a real value, not “empty” in the encoder’s eyes.


Why false, 0, and empty strings disappear

API-style structs often tag every optional field with omitempty:

go
type Settings struct {
	Enabled bool   `json:"enabled,omitempty"`
	Limit   int    `json:"limit,omitempty"`
	Name    string `json:"name,omitempty"`
}

Marshaling a zero Settings produces {} because false, 0, and "" are all empty under legacy omitempty.

When zero is meaningful in your API—distinguishing “not provided” from “provided as false or 0”—omitempty is the wrong tool. That is a presence problem: use *bool, *int, or another explicit optional representation, and drop omitempty on fields where false or 0 must appear in JSON.


Slices, maps, pointers, and interfaces

Under legacy encoding/json, both nil and a non-nil empty slice or map have length zero, so omitempty omits them:

go
package main

import (
	"encoding/json"
	"fmt"
)

type Order struct {
	Items []string          `json:"items,omitempty"`
	Meta  map[string]string `json:"meta,omitempty"`
}

func main() {
	b, _ := json.Marshal(&Order{})
	fmt.Println(string(b))
}
text
{}

A nil pointer is also empty, which is why *T is the usual way to mean “this nested object was not set.” That distinction becomes important when you compare omitempty with omitzero below.


Why omitempty does not omit a zero nested struct

A nested value struct is never nil. Its zero value marshals as a JSON object, so the outer key stays:

go
package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	City  string `json:"city,omitempty"`
	State string `json:"state,omitempty"`
}

type User struct {
	Name    string  `json:"name,omitempty"`
	Address Address `json:"address,omitempty"`
}

func main() {
	b, _ := json.Marshal(&User{})
	fmt.Println(string(b))
}
text
{"address":{}}

Inner omitempty tags still hide zero inner fields, but address itself remains because the struct value is not empty at the outer level.

Historically, the fix was a pointer—Address *Address with nil omits the whole key. That still matters when absence is different from “present but empty.” On Go 1.24+, you can also use:

go
Address Address `json:"address,omitzero"`

Marshaling User with a zero Address and omitzero prints {} because the nested struct equals its Go zero value.


omitempty vs omitzero in Go 1.24+

Go 1.24 added omitzero to encoding/json. It omits a field when:

  1. the type has an IsZero() bool method that returns true; otherwise
  2. the field equals its Go zero value.

omitempty still uses the legacy “empty Go value” rules above. Use this table when choosing a tag:

Value omitempty omitzero
int(0) omit omit
false omit omit
"" omit omit
nil slice omit omit
non-nil []T{} omit keep
nil map omit omit
non-nil empty map omit keep
zero struct keep omit

Use omitzero when your intent is specifically to omit Go zero values; keep omitempty when JSON emptiness—such as an empty non-nil slice or map—is what you want. The Go 1.24 release notes describe omitzero as clearer for zero-value omission, not as a universal replacement for omitempty.

Zero structs and time.Time

time.Time is the common real-world case. Legacy omitempty does not drop a zero timestamp:

go
type Event struct {
	CreatedAt time.Time `json:"created_at,omitempty"`
}

Marshaling Event{} still includes "created_at":"0001-01-01T00:00:00Z" because a zero time.Time is a non-empty struct under omitempty.

With omitzero, time.Time.IsZero() applies:

go
CreatedAt time.Time `json:"created_at,omitzero"`

Marshaling a zero Event then yields {}.

Empty vs nil slices and maps

omitempty treats nil and []T{} the same—both omitted. omitzero omits only nil slices and maps; a non-nil empty collection encodes as [] or {} and stays in the output. Pick omitzero when you need to preserve “present but empty” in JSON.


Go 1.27: encoding/json/v2 changes omitempty

Go 1.27 ships stable encoding/json/v2. Existing code can keep using encoding/json; the v1 API remains supported. New code and migrations should know that v2 redefines omitempty.

Legacy encoding/json

omitempty means empty Go values—including false and numeric 0.

encoding/json/v2

omitempty means the field would encode as an empty JSON value: null, "", {}, or []. A Go false or 0 normally encodes as JSON false or 0, so those keys stay under v2 omitempty. Although v2 changes the default JSON representation of nil slices and maps—they marshal as [] and {} rather than nullomitempty still omits them because those encodings are empty JSON arrays and objects.

go
import jsonv2 "encoding/json/v2"

type Settings struct {
	Enabled bool `json:"enabled,omitempty"`
	Limit   int    `json:"limit,omitempty"`
}

// jsonv2.Marshal(Settings{}) → {"enabled":false,"limit":0}

Migration guidance

For bools, numbers, pointers, and interfaces where you want traditional zero-value omission, upstream guidance recommends omitzero—it works in both v1 and v2 with the same Go-zero semantics. The compatibility options in encoding/json include json.OmitEmptyWithLegacySemantics(true) for preserving v1-style omitempty behavior during migration. See the encoding/json/v2 package documentation and Go 1.27 release notes for options; this page is not a full json/v2 migration guide.


Common omitempty mistakes

Problem Explanation / fix
Nested struct still appears as {} Legacy omitempty does not treat a struct as empty; use *T with nil or Go 1.24+ omitzero
false disappears Legacy omitempty treats false as empty during Marshal
0 disappears Same for numeric zero
Need to distinguish absent from zero Use pointers or another explicit presence model; drop omitempty on those fields
Zero time.Time still appears Use omitzero on Go 1.24+
Expected non-nil empty slice to survive Legacy omitempty omits length-zero slices; use omitzero to keep []
Assuming v1 and v2 behave identically Go 1.27 encoding/json/v2 changes omitempty to JSON-empty rules
Expecting the tag to change unmarshaling omitempty and omitzero affect Marshal only

Summary

Legacy encoding/json omitempty skips keys when a field holds an empty Go value—false, 0, "", nil pointers, and zero-length arrays, slices, or maps—during marshaling only. It does not change how json.Unmarshal fills struct fields when keys are missing.

The painful cases are zero nested structs and zero time.Time values, where omitempty keeps the key. Pointers still express optional presence; Go 1.24+ omitzero omits zero structs and zero timestamps when that is all you need. If you adopt Go 1.27’s encoding/json/v2, remember that v2 omitempty follows JSON emptiness, not Go zero values—use omitzero for bool and number fields that should disappear at zero. For reading and decoding JSON, see json.Unmarshal in Go.


References


Frequently Asked Questions

1. What does omitempty mean in Go JSON?

In encoding/json, json:"field,omitempty" omits the field during Marshal when its Go value is empty—false, 0, empty string, nil pointer or interface, or zero-length slice, array, or map. It does not affect Unmarshal.

2. Why does not omitempty remove a nested struct?

A non-pointer nested struct is never nil. Its zero value still marshals as a JSON object like {}, so legacy omitempty keeps the outer key. Use a nil pointer when absence matters, or omitzero on Go 1.24+ to drop a zero struct value.

3. What is the difference between omitempty and omitzero?

omitempty in encoding/json uses empty Go values. omitzero omits when the field is the Go zero value or its IsZero method returns true. They differ for zero structs, zero time.Time, and non-nil empty slices or maps.

4. How do I omit a zero time.Time field?

Legacy omitempty does not omit a zero time.Time because the struct is not considered empty. On Go 1.24+, use json:"field,omitzero" so time.Time.IsZero controls omission.

5. Why does false or 0 disappear from my JSON?

Legacy encoding/json omitempty treats false, numeric zero, and empty string as empty during Marshal, so those keys are left out even when the values are meaningful in your API.

6. Does omitempty affect json.Unmarshal?

No. omitempty and omitzero apply only when marshaling to JSON. If a key is missing during Unmarshal, that field is left unchanged; for a newly zero-valued struct, this means it remains at its Go zero value.

7. Does omitempty work the same in encoding/json/v2?

No. In Go 1.27 encoding/json/v2, omitempty omits fields that would encode as JSON null, empty string, empty object, or empty array—so false and 0 are usually kept. Use omitzero when you want Go zero-value omission in v2.
Tuan Nguyen

Data Scientist

Proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise spanning these technologies, he develops robust solutions and implements efficient data processing and management strategies across various projects and platforms.

  • Go (programming language)
  • Python (programming language)
  • Java (programming language)
  • MongoDB
  • Kubernetes