skip to content

How does encoding/json's omitzero option differ from omitempty, and when do the two disagree?

level: middleimportance: should knowfreq 42%

answer

  1. one checks a list, one checks the zero
  2. IsZero gets a vote
  3. the zero time only one drops
  4. an allocated empty slice is not nil
  5. they disagree in both directions

basics

~20 s

In encoding/json, omitzero (Go 1.24) omits a field holding its type's zero value, using an IsZero() bool method when there is one. omitempty omits a fixed list of empty values instead, so the two disagree on zero structs and on non-nil empty slices.

solid answer

~50 s

`omitempty` checks the value against a fixed list the encoder calls empty: `false`, numeric zero, `""`, nil pointer or interface, and any length-zero array, slice, map or string. `omitzero`, added to `encoding/json` in Go 1.24, asks a different question: is this the zero value for the field's type, and if the type has an `IsZero() bool` method, what does that method say. They agree on `0`, `false`, `""` and nil pointers. They disagree in both directions: a `time.Time` holding the zero time is omitted by `omitzero` because `IsZero` returns true, but kept by `omitempty` because a struct is never empty; a non-nil empty slice like `[]string{}` is omitted by `omitempty` but kept by `omitzero`, since the zero value of a slice is nil, not an empty one. If a tag carries both options the field is omitted when either rule fires.

code

go · 9 lines
go
type Job struct {
	Started  time.Time `json:"started,omitzero"`
	Finished time.Time `json:"finished,omitempty"`
	Tags     []string  `json:"tags,omitzero"`
	Labels   []string  `json:"labels,omitempty"`
}

// json.Marshal(Job{Tags: []string{}, Labels: []string{}}) produces:
// {"finished":"0001-01-01T00:00:00Z","tags":[]}

go deeper

for a junior

Know that a second option exists beside omitempty and that it is about the zero value of the type rather than a fixed list. Being able to name the zero time.Time case is enough at this level.

for a middle

Give both predicates precisely and both directions of disagreement, and mention that a type's IsZero method is consulted. Say which Go version introduced the newer option.

for a senior

Talk about what changes on the wire when you swap the option across an existing struct. An allocated empty slice appearing where it used to vanish is a contract change for every consumer, even though the Go code looks identical.

for a principal

Own the toolchain question: the newer option is silently ignored on older toolchains, so adopting it across shared wire structs means setting a floor in go.mod and knowing which consumers build below it before the tags change meaning.

## Two different questions Both options live in the same place — after the comma in a `json:"name,..."` struct tag — and both are consulted only when encoding. What differs is the predicate. `omitempty` asks: **is this value on the encoder's empty list?** That list is fixed and has been stable for years: `false`, any numeric zero, the empty string, a nil pointer, a nil interface value, and any array, slice, map or string of length zero. `omitzero`, added in Go 1.24, asks: **is this the zero value of the field's type?** If the field's type (or its pointer type) has a method `IsZero() bool`, the encoder calls it and omits the field when it returns true. Otherwise it compares against the type's zero value directly. ## Where they agree For the everyday scalar cases the two behave identically. An `int` holding `0`, a `bool` holding `false`, a `string` holding `""`, a nil pointer, a nil map and a nil slice are all dropped by either option. If your struct is nothing but scalars and pointers, swapping one for the other changes nothing. ## Where they disagree, in both directions **A zero struct.** `time.Time{}` is the canonical case. It is a struct, so it is not on `omitempty`'s list and the field is written as `"0001-01-01T00:00:00Z"`. It *is* the zero value of `time.Time`, and `time.Time` has an `IsZero() bool` method that returns true for it, so `omitzero` drops the field. The same applies to any struct type you defined: `omitempty` never omits it, `omitzero` omits it when every field is zero (or when your own `IsZero` says so). **A non-nil empty collection.** `[]string{}` and `map[string]int{}` have length zero, so `omitempty` drops them. But the zero value of a slice or map type is `nil`, and an allocated empty one is not nil, so `omitzero` **keeps** them and writes `[]` or `{}`. This is the direction people forget, and it is occasionally what you want: an API where `[]` means "the list is now empty" and an absent key means "leave the list alone" is expressible with `omitzero` and not with `omitempty`. **A pointer to a zero value.** Both options keep it. `omitempty` keeps it because a non-nil pointer is not on the list; `omitzero` keeps it because a non-nil pointer is not the zero value of `*T`. This is why the `*T` idiom for optional fields works under either tag. ## Using both together A tag may carry both, as in `json:"tags,omitempty,omitzero"`. The field is then omitted if **either** rule fires — an empty-by-the-list value or a zero value. That covers a type where you want both a nil and an allocated-but-empty collection gone, or a struct that is both zero and, by some other measure, empty. ## The IsZero hook The `IsZero() bool` hook is what makes `omitzero` composable. Any type you own can declare what "zero" means for itself — a wrapper around a decimal that is zero when the amount is zero, an ID type that is zero when unset — and `encoding/json` will honour it without a custom marshaler. That is a much smaller commitment than implementing `json.Marshaler`, which takes over the entire encoding of the value rather than just the decision to include it. ## Version discipline `omitzero` requires a Go 1.24 or newer toolchain. On an older toolchain the tag is not an error: `encoding/json` ignores tag options it does not recognise, so the field is simply always written. That is a quiet failure mode, and it is worth knowing when a library is built and tested on a recent toolchain but consumed by a module still on an older one, or when a build pipeline pins something older than the developer machines. ## What to say in an interview Define both predicates in one sentence each, then give the two disagreements in opposite directions — the zero `time.Time` that only `omitzero` drops, and the allocated empty slice that only `omitempty` drops. Mention `IsZero` and the Go 1.24 requirement. That answer distinguishes someone who has read the current release notes from someone working from a mental model that stopped a few versions ago.

  • Why does omitzero keep a []string{} that omitempty drops?
    The zero value of a slice type is `nil`, not an allocated slice of length zero. `[]string{}` is non-nil, so it is not the zero value and `omitzero` writes `[]`. `omitempty` uses length instead, sees zero, and drops the key. If you want an explicit empty list to reach the client, `omitzero` is the option that lets it through.
  • How does a type of your own get omitted by omitzero when it is not literally all-zero?
    Give it an `IsZero() bool` method. `encoding/json` calls that method when the option is present and omits the field when it returns true, so the type decides what unset means for itself — an amount type that is zero at zero, an ID type that is zero when blank. It is much less invasive than implementing `json.Marshaler`, which would take over encoding of the whole value.
  • What happens if a tag carries both omitempty and omitzero?
    The field is omitted when either predicate is satisfied — empty by the encoder's list, or the zero value for the type. That combination is useful for a slice field where you want both a nil slice and an allocated empty one to disappear, since neither option alone covers both cases.

saying these in an interview costs you the question

  • Says omitzero is just a renamed omitempty
  • Thinks omitzero drops a non-nil empty slice
  • Believes omitempty drops a zero time.Time
  • Assumes an unknown tag option fails the build on an older toolchain
  • Does not know IsZero is consulted by omitzero