| 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:
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:
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))
}{}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:
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))
}{"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:
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:
- the type has an
IsZero() boolmethod that returns true; otherwise - 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:
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:
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 null—omitempty still omits them because those encodings are empty JSON arrays and objects.
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
- Package encoding/json
- Go 1.24 release notes —
omitzero - Package encoding/json/v2
- Go 1.27 release notes — encoding/json/v2

