skip to content

What does the struct tag json:"user_id,omitempty" change about how encoding/json handles that field?

level: juniorimportance: must knowfreq 82%

answer

  1. the value is a list, not a name
  2. first element names, the rest configure
  3. one option only touches encoding
  4. a lone dash means gone entirely
  5. a trailing comma changes what dash means

basics

~20 s

The tag renames the field to user_id in JSON, on both encoding and decoding. The omitempty option additionally drops the field from the encoder's output when its value is false, 0, nil, or an empty string, slice, array or map.

solid answer

~50 s

A json struct tag is a comma-separated list: the first element is the JSON name, everything after it is an option. So `json:"user_id,omitempty"` means the field is written as `user_id` and read from `user_id`, and `omitempty` tells `json.Marshal` to leave the key out when the value is the empty one for its kind — `false`, `0`, an empty string, an empty slice, array or map, or a nil pointer or interface. `omitempty` affects encoding only; it has no meaning while decoding. Three forms are worth memorising: `json:",omitempty"` keeps the Go field name and applies the option; `json:"-"` removes the field from JSON entirely in both directions; and `json:"-,"` — with the trailing comma — is how you name a field the single character `-`. Note that a struct value is never considered empty, however many zero fields it has.

code

go · 10 lines
go
type Account struct {
	ID       int    `json:"id"`
	Nickname string `json:"nickname,omitempty"`
	Internal string `json:"-"`
	Legacy   string `json:"-,"`
	Region   string `json:",omitempty"`
}

// json.Marshal(Account{ID: 7, Region: "eu"})
// {"id":7,"-":"","Region":"eu"}

go deeper

for a junior

Be able to read a tag out loud: name first, then options. Know that the tag governs both directions, that omitempty drops empty values on output, and that json:"-" hides a field completely.

for a middle

Explain the exact empty set omitempty uses and why a struct or time.Time field is never in it. Know that an unrecognised option is silently ignored and that json:",omitempty" keeps the Go name.

for a senior

Show that omitempty cannot distinguish an unset field from a zero one, and say when that ambiguity is acceptable on a wire contract you own versus when it is a defect to design out.

for a principal

Own the convention: whether tags are hand-written or generated, whether omitempty is default or exceptional across the codebase, and how the team keeps the JSON names stable while Go identifiers keep changing.

## The shape of a tag A struct tag is a single string literal, conventionally written in backquotes, holding space-separated `key:"value"` pairs: ```go Amount int `json:"amount,omitempty" xml:"amount"` ``` Each library looks up its own key. `encoding/json` reads the `json` key and splits its value on commas: - **element 0** — the JSON name for this field - **elements 1..n** — options ## The name If the name is a non-empty valid string, it replaces the Go field name in the JSON, **in both directions**: `json.Marshal` writes that key, and `json.Unmarshal` matches on it. The Go identifier and the wire name are therefore completely decoupled, which is what lets you have idiomatic Go (`UserID`) and an idiomatic wire format (`user_id`) at once. If the name is empty — `json:",omitempty"` — the Go field name is used, capital letter and all. This form is easy to write by accident when you meant `json:"user_id,omitempty"` and dropped the name. The name may contain Unicode letters, digits and most ASCII punctuation, but not a quote, a backslash or a comma. A name that violates that is ignored and the Go field name is used instead. ## The dash, and the dash-comma ```go Password string `json:"-"` // never encoded, never decoded Weird string `json:"-,"` // JSON key is literally "-" ``` `json:"-"` is the standard way to keep a field out of the JSON representation of a struct — it disappears from output and is ignored on input. Because `-` is a legal JSON key, the library needs an escape hatch for the (rare) field that really is named `-`, and that is the trailing comma: `json:"-,"` parses as name `-` with an empty option list. ## omitempty `omitempty` drops the key from the **encoder's output** when the field holds the empty value for its kind: | kind | considered empty when | |---|---| | bool | false | | int, uint, float | 0 | | string | "" | | slice, array, map | length 0 (a nil slice is also length 0) | | pointer, interface | nil | Two things it does **not** do: 1. **It does nothing on decode.** The option lives entirely in the encoder. Decoding `{}` into a struct simply leaves every field at its zero value, with or without the option. 2. **It does not look inside a struct.** A struct-typed field whose every member is zero is *not* empty, so `omitempty` will not remove it. The same goes for a `time.Time` field, which is a struct. This surprises people constantly, and it is the reason a "clean" payload still carries `"created_at":"0001-01-01T00:00:00Z"`. Also worth stating plainly: `omitempty` cannot distinguish *absent* from *zero*. A field holding `0` and a field the caller never set look identical to the encoder, and both vanish. Whether that is acceptable is a design question about the wire contract, not about the tag. ## Order and unknown options The name must come first; options after it are order-independent. An option `encoding/json` does not recognise is ignored silently — there is no error and no warning, which is exactly why a typo like `omitempy` costs a debugging session. ## Putting it together ```go type Account struct { ID int `json:"id"` Nickname string `json:"nickname,omitempty"` Internal string `json:"-"` Legacy string `json:"-,"` Region string `json:",omitempty"` } ``` Marshaling `Account{ID: 7, Region: "eu"}` produces: ```json {"id":7,"-":"","Region":"eu"} ``` Reading that output line by line is the fastest way to internalise the grammar. `id` is a plain rename. `nickname` is gone because the string is empty and it carries `omitempty`. `Internal` is gone unconditionally. `Legacy` is present under the key `-`, with an empty value, because it has no `omitempty`. `Region` kept its Go name — capital R and all — because the tag gave an option but no name. ## Where the tag stops Field order in the output follows declaration order, not the tag. Tags do not convert types, validate anything, or provide defaults; they name a field and toggle a small fixed set of options. And a tag is not checked by the compiler at all — it is a string literal that either parses at run time or does not, which is why `go vet`'s structtag analyzer is worth having in CI.

  • Does omitempty change anything about how json.Unmarshal decodes into that field?
    No. `omitempty` is purely an encoder instruction. When decoding, a key that is absent from the input simply leaves the field at whatever it already held — usually its zero value — exactly as it would without the option. Nothing in the decoder consults it.
  • A struct field whose members are all zero still appears in the output despite omitempty. Why?
    Because `omitempty` has a fixed notion of empty that covers false, zero numbers, empty strings, zero-length slices, arrays and maps, and nil pointers and interfaces — struct kinds are not on that list. An all-zero struct, including a zero `time.Time`, is therefore always written.
  • How do you keep a field out of JSON entirely, and how do you name a field the single character -?
    `json:"-"` removes the field from both encoding and decoding. To publish a field under the literal key `-`, write `json:"-,"` with a trailing comma, which parses as the name `-` followed by an empty option list. Without that comma the library reads it as the skip directive.

saying these in an interview costs you the question

  • Thinks omitempty also affects decoding
  • Expects omitempty to drop an all-zero struct or zero time.Time
  • Writes json:",omitempty" and expects the field renamed
  • Assumes json:"-" only hides the field on output
  • Believes an unrecognised option raises an error