skip to content

Nulls, Omitted and Defaults

omitempty cannot tell an absent field from a zero one, so a false or a 0 vanishes from the payload. Pointers, RawMessage and the newer omitzero option are the ways out.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

In encoding/json, what does the omitempty struct tag option omit, and what does it leave in?

level: juniorimportance: must knowfreq 78%

answer

  1. the encoder's own list of empty
  2. false, zero, empty string, nil
  3. length-zero slices, maps, strings
  4. a struct is never empty
  5. the zero time still ships

basics

~20 s

In encoding/json, omitempty skips a field whose value is false, 0, an empty string, a nil pointer or interface, or an empty array, slice or map. It never skips a zero-valued struct such as time.Time{}, and it only affects encoding.

solid answer

~40 s

`omitempty` is consulted only by `json.Marshal`. It drops the field when the value is what the encoder calls empty: `false`, any numeric zero, `""`, a nil pointer, a nil interface, or an array, slice, map or string of length zero. Everything else is written, and crucially a struct is never empty by this definition, so a `time.Time` holding the zero time still marshals as `"0001-01-01T00:00:00Z"`. A non-nil pointer to a zero value is also written, which is exactly why `*T` is the usual way to keep a legitimate `false` or `0` on the wire while still omitting an unset field. `omitempty` has no effect on decoding at all, and it cannot express "the client sent this explicitly" — it collapses a real zero and an unset field into the same absent key.

code

go · 8 lines
go
type Settings struct {
	Debug   bool      `json:"debug,omitempty"`
	Retries int       `json:"retries,omitempty"`
	Since   time.Time `json:"since,omitempty"`
}

// json.Marshal(Settings{}) produces:
// {"since":"0001-01-01T00:00:00Z"}

go deeper

for a junior

Be ready to recite the empty list on demand: false, numeric zero, empty string, nil pointer or interface, and any length-zero array, slice, map or string. Then say the one exception out loud, that a struct is never empty.

for a middle

Explain that the option runs only at encode time and that it collapses a deliberate zero with an unset field, then show the pointer fix and say why a non-nil pointer to false survives the check.

for a senior

Show the production consequence: a bool or int tagged omitempty quietly changes what a message means to its consumer. Say how you would audit a wire struct for that and what you would put in the type instead.

for a principal

Frame it as a contract decision. Whether an absent key means unchanged, default or invalid is something the API owner must state once, and the tag choices across every wire struct have to follow that statement rather than each author's taste.

## What the option is In a Go struct tag such as `json:"retries,omitempty"`, the part before the comma is the JSON name and everything after it is a list of options. `omitempty` is one of those options, and it is read by `encoding/json` at **encoding** time only: `json.Marshal` and `json.Encoder.Encode` consult it, `json.Unmarshal` and `json.Decoder.Decode` ignore it entirely. ## The encoder's definition of "empty" This is the whole question, because "empty" is a specific list and not a general idea of emptiness. `encoding/json` omits the field when the value is: - `false` - any numeric zero (`0`, `0.0`, and the zero of any sized integer or float type) - the empty string `""` - a nil pointer - a nil interface value - an array, slice, map or string whose length is zero Note what is *not* on that list: **a struct**. A struct value is never considered empty, no matter what is inside it. Neither is a non-nil pointer, a channel, or a function value. ## The two traps this creates **The zero struct.** The most common surprise is `time.Time`. A `time.Time` is a struct, so `omitempty` does nothing for it: a field holding the zero time marshals as `"0001-01-01T00:00:00Z"` and lands on the wire, usually confusing whatever consumes it. The same applies to any struct you wrote yourself and to fixed-size arrays that are the right length but full of zeros. The classic workarounds are to make the field a `*time.Time` (nil is empty, so it is omitted) or, on Go 1.24 and newer, to use the `omitzero` option instead, which is defined against the zero *value* rather than against this list and does consult a type's `IsZero() bool` method. **The meaningful zero.** `omitempty` cannot tell "the caller did not set this" from "the caller set it to zero". A `bool` field with `omitempty` disappears whenever it is `false`, so a client that deliberately turns a flag off produces the same JSON as a client that never mentioned it. A `Count int` with `omitempty` cannot transmit a genuine `0`. If either of those distinctions matters, the field must not be a bare value type: a `*bool` is nil when unset and points at `false` when explicitly set, and a non-nil pointer is never empty, so the `false` survives. ## Why the option exists at all JSON has no notion of a default, so an encoder either writes every field or is told which ones to skip. `omitempty` keeps payloads small and keeps optional fields out of documents where their presence would be read as meaningful — a partial update, a config document, an API that treats an absent key as "do not change this". Used on a pointer or a slice it is exactly right. Used on a plain `bool` or `int` it silently changes the meaning of the message. ## Adjacent facts worth having ready - The option only applies to the field it is written on; it is not inherited and it does not recurse into a nested struct's fields. - It does not suppress the field for a custom marshaler in the way people expect: if the field's type implements `json.Marshaler` and the value is a non-empty-by-this-list value, the method runs and its output is written even if that output happens to be `null`. - `omitempty` is not a validation mechanism. Omitting a field from the output says nothing about whether the receiver requires it. - Unknown tag options are ignored by `encoding/json`, so a typo such as `omitEmpty` or `omit_empty` silently does nothing — the field is simply always written, which is a bug that survives review easily because nothing errors. ## What to say in an interview Give the list, then immediately give the two things it does not cover: a zero struct like `time.Time{}`, and the difference between an unset value and a deliberate zero. Naming `*T` as the fix for the second, and `omitzero` (Go 1.24) or `*time.Time` as the fix for the first, is what turns a recalled definition into an answer that shows you have shipped a JSON API.

  • A time.Time field tagged omitempty keeps showing up as 0001-01-01T00:00:00Z. What are your options?
    Either change the field to `*time.Time`, so an unset value is a nil pointer and the encoder's empty rule applies, or use the `omitzero` option instead of `omitempty`, added in Go 1.24, which omits a field that is the zero value for its type and uses the type's `IsZero() bool` method when it has one. `time.Time` has `IsZero`, so `omitzero` drops it.
  • Why does a bool field with omitempty break an API that needs to send an explicit false?
    The encoder treats `false` as empty, so the key is dropped and the receiver cannot tell "turn this off" from "never mentioned". Making the field a `*bool` fixes it: nil means unset and is omitted, while a non-nil pointer to `false` is not empty by the encoder's list, so `"flag":false` is written.
  • Does omitempty change anything about decoding?
    No. `json.Unmarshal` reads the name in the tag but ignores `omitempty` completely. An absent key simply leaves the Go field at whatever value it already had, which for a fresh struct is the zero value. If you need to know whether a key was present, the tag option cannot tell you — the field type has to carry that, typically as a pointer or a wrapper type.

It is a skip list, not a judgement of meaning: the encoder checks the value against six shapes it calls empty, and anything not on that list gets written out however hollow it looks.

saying these in an interview costs you the question

  • Thinks omitempty omits a zero-valued struct such as time.Time{}
  • Believes omitempty also affects json.Unmarshal or rejects absent keys
  • Says omitempty can distinguish an unset field from a deliberate false
  • Expects a non-nil pointer to zero to be omitted
  • Assumes a misspelled tag option like omitEmpty errors at build time
open as a page

When json.Unmarshal meets a JSON null, which Go field types does it set to nil and which does it leave untouched?

level: middleimportance: should knowfreq 52%

basics

~20 s

In encoding/json, a JSON null sets a pointer, interface, map or slice field to nil. For any other Go type, including numbers, strings and structs, it is a documented no-op: the field keeps its value and no error is returned.

open as a page

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

level: middleimportance: should knowfreq 42%

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.

open as a page

In a Go patch API, how do you tell an omitted JSON field from an explicit null when decoding?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A *T field cannot do it, because encoding/json leaves it nil for an absent key and also sets it to nil for an explicit null. Three states need a value-typed wrapper with an UnmarshalJSON method, a json.RawMessage field, or a first pass into map[string]json.RawMessage.

open as a page

Why does a struct with a sql.NullString field marshal to a JSON object instead of a string or null?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

sql.NullString is an ordinary two-field struct and implements neither json.Marshaler nor encoding.TextMarshaler, so encoding/json applies its default struct rules and writes {"String":"","Valid":false}. It also fails to decode a bare JSON string. Use a *string in the wire type instead.

open as a page