In Go's encoding/json, the omitempty tag option leaves a field out of the JSON when its value is false, 0, a nil pointer, a nil interface, or an empty string, slice, map or array. It never omits a struct, so a zero time.Time is still written as "0001-01-01T00:00:00Z". The omitzero option, added in Go 1.24, leaves a field out when it holds the zero value of its type, and uses the type's IsZero() method when there is one, so it does omit an unset time.Time. Neither can tell "zero" from "not set": when 0 or false is a real answer, use a pointer.

Since Go 1.27 the encoding/json package runs on the new encoding/json/v2 implementation, but it keeps the old behaviour, including the old meaning of omitempty. The new package is where the meaning changes, as shown below.

What Each Tag Writes, Measured

We marshaled a struct with every field left at its zero value (Go 1.27.3, encoding/json), plus one pointer to 0:

FieldTagValueWritten to JSON?
Count intomitempty0No
Active boolomitemptyfalseNo
Name stringomitempty""No
When time.Timeomitemptyzero timeYes, "0001-01-01T00:00:00Z"
When time.Timeomitzerozero timeNo
Addr Address (a struct)omitemptyzero structYes, {"city":""}
Addr Addressomitzerozero structNo
Tags []stringomitempty[]string{} (empty, not nil)No
Tags []stringomitzero[]string{} (empty, not nil)Yes, []
Ptr *intomitemptynilNo
Ptr *intomitemptypointer to 0Yes, 0
Ptr *intno optionnilYes, null

Two rows are easy to miss. A struct is never "empty" to omitempty, which is why APIs written in Go so often send 0001-01-01T00:00:00Z for a date that was never set. And an empty slice that is not nil is not the zero value of a slice, so omitzero keeps it as [], while omitempty drops it. If you want both behaviours, Go allows json:"tags,omitempty,omitzero", which omits the field when it is either empty or zero.

When a Zero Is a Real Answer, Use a Pointer

omitempty on a plain int cannot send a discount of 0, a quantity of 0 or "enabled": false: those values disappear, and a client that treats a missing field as "use the default" may do something different from what you meant. Make the field a pointer:

1
2
3
4
type Settings struct {
	Discount *int  `json:"discount,omitempty"`
	Enabled  *bool `json:"enabled,omitempty"`
}

Now nil means "not set" and is left out, while a pointer to 0 or false is written. In our run, a *int pointing to 0 with omitempty produced "ptr":0, and a nil one produced nothing.

Reading JSON: Missing, null and a Value

Decoding has its own rules, and omitempty and omitzero play no part in them.

  • A missing key leaves the field as it was, usually its zero value.
  • null into a pointer, slice, map or interface sets it to nil.
  • null into a plain string, int, bool or struct does nothing in encoding/json. We decoded {"name": null, "age": null} into a value that already held "keep" and 5; both were unchanged and no error was returned.

So with a *string field, a missing key and null both give nil. For most responses that is fine. For a PATCH request, where "nickname": null means "clear it" and a missing key means "leave it", you need a type that records whether the key was present. UnmarshalJSON is called for null but not for a missing key, so a small wrapper is enough:

1
2
3
4
5
6
7
8
9
10
11
12
13
type OptString struct {
	Set, Null bool
	Value     string
}

func (o *OptString) UnmarshalJSON(b []byte) error {
	o.Set = true
	if string(b) == "null" {
		o.Null = true
		return nil
	}
	return json.Unmarshal(b, &o.Value)
}

Decoding {}, {"nickname": null} and {"nickname": "Ada"} into a field of this type gave Set:false, then Set:true Null:true, then Set:true Value:Ada: three states, told apart.

Numbers: Where Generated Structs Usually Break

JSON has one number type, and Go has many. Three measured cases:

  • 10.0 into an int is an error. json.Unmarshal refused both 10.0 and 10.5 for an int field ("cannot unmarshal number 10.5 into Go struct field ... of type int"). A struct generated from a sample where the price happened to be whole will fail on the first fractional price.
  • Numbers in interface{} become float64. 9007199254740993 decoded into a map[string]interface{} came back as 9007199254740992, because a float64 holds integers exactly only up to 2^53. RFC 8259 warns about this range. Decode IDs into int64, which kept the exact value in our test, or call Decoder.UseNumber().
  • Some APIs send numbers as strings ("id": "123"). The string tag option, json:"id,string", decodes those into an int64.

Two Defaults That Hide Mistakes

Unknown keys are ignored. A typo in a tag, or a field the server added, decodes without an error. While testing, decode with a json.Decoder and call DisallowUnknownFields(); it returned json: unknown field "extra" in our run.

Key matching ignores case. encoding/json matched the key "NAME" to a field tagged json:"name". It does not ignore underscores, though: "zip_code" did not reach a field named ZipCode that had no tag, which is why generated structs carry a tag on every field.

What Changes in encoding/json/v2 (Go 1.27)

Go 1.27 added the encoding/json/v2 and encoding/json/jsontext packages, and the old encoding/json is now implemented on top of v2 with options that keep v1 behaviour. Your existing code keeps working. If you call the v2 package directly, its defaults differ, and we saw each of these on Go 1.27.3:

Behaviourencoding/json (v1)encoding/json/v2
omitempty on int with value 0OmittedWritten as 0 (v2 omits only values that encode as null, "", {} or [])
nil slicenull[]
null into a non-empty string / intValue keptValue set to zero
Key "NAME" for a field tagged nameMatchedNot matched (case-sensitive)
Duplicate keys in an objectLast one winsError

The Go documentation's advice for code that should behave the same under both: keep omitempty for strings, slices, maps and arrays, where both versions agree, and use omitzero for numbers, booleans, pointers, interfaces and structs.

Choosing, in One Table

You wantUse
Leave out empty strings, slices and mapsomitempty
Leave out an unset time.Time or nested structomitzero
Leave out a number or bool only when it was never setA pointer with omitempty (or omitzero)
Always send the field, even when zeroNo option
Tell "missing" from null when decodingA type with its own UnmarshalJSON
Code that behaves the same under v1 and v2omitzero for numbers, bools, pointers and structs

Start From a Generated Struct, Then Decide Field by Field

The FindUtils JSON to Go Struct generator writes the types and tags from a sample response, and its Omit Empty option adds ,omitempty to every field at once. From there, the decisions above are per field: which numbers should be float64 or int64, which fields need a pointer, which dates need omitzero. The JSON to Go Struct guide lists what to check in the generated output. To see which fields come and go between two responses, compare them in JSON Diff; to send a test request and look at the raw response, use the HTTP Request Builder.

Need help with a project? The team behind FindUtils takes on freelance work, such as a fast REST API or back-end service built in Go.

FAQ

What is the difference between omitempty and omitzero in Go?

omitempty omits false, 0, nil pointers and interfaces, and empty strings, slices, maps and arrays, but never a struct. omitzero omits the zero value of any type, including structs, and uses an IsZero() method when the type has one. An empty but non-nil slice is omitted by omitempty and kept by omitzero.

Why does my Go API return 0001-01-01T00:00:00Z?

That is the zero time.Time. omitempty does not omit structs, and time.Time is a struct. Use json:"field,omitzero" (Go 1.24 or later) or a *time.Time that stays nil when the date is not set.

How do I send 0 or false with omitempty?

Use a pointer type such as *int or *bool. A nil pointer is omitted, and a pointer to 0 or false is written.

Does omitempty affect json.Unmarshal?

No. Both options only affect marshaling. When decoding, a missing key leaves the field unchanged.

Do I need to change my code for Go 1.27?

Usually not. encoding/json keeps its behaviour, although error message text may differ. If you switch to encoding/json/v2, review omitempty on numbers, bools and pointers, nil slices, and case-insensitive key matching.

Sources