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:
| Field | Tag | Value | Written to JSON? |
|---|---|---|---|
Count int | omitempty | 0 | No |
Active bool | omitempty | false | No |
Name string | omitempty | "" | No |
When time.Time | omitempty | zero time | Yes, "0001-01-01T00:00:00Z" |
When time.Time | omitzero | zero time | No |
Addr Address (a struct) | omitempty | zero struct | Yes, {"city":""} |
Addr Address | omitzero | zero struct | No |
Tags []string | omitempty | []string{} (empty, not nil) | No |
Tags []string | omitzero | []string{} (empty, not nil) | Yes, [] |
Ptr *int | omitempty | nil | No |
Ptr *int | omitempty | pointer to 0 | Yes, 0 |
Ptr *int | no option | nil | Yes, 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:
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.
nullinto a pointer, slice, map or interface sets it tonil.nullinto a plainstring,int,boolor struct does nothing inencoding/json. We decoded{"name": null, "age": null}into a value that already held"keep"and5; 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:
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.0into anintis an error.json.Unmarshalrefused both10.0and10.5for anintfield ("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{}becomefloat64.9007199254740993decoded into amap[string]interface{}came back as9007199254740992, because afloat64holds integers exactly only up to 2^53. RFC 8259 warns about this range. Decode IDs intoint64, which kept the exact value in our test, or callDecoder.UseNumber(). - Some APIs send numbers as strings (
"id": "123"). Thestringtag option,json:"id,string", decodes those into anint64.
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:
| Behaviour | encoding/json (v1) | encoding/json/v2 |
|---|---|---|
omitempty on int with value 0 | Omitted | Written as 0 (v2 omits only values that encode as null, "", {} or []) |
| nil slice | null | [] |
null into a non-empty string / int | Value kept | Value set to zero |
Key "NAME" for a field tagged name | Matched | Not matched (case-sensitive) |
| Duplicate keys in an object | Last one wins | Error |
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 want | Use |
|---|---|
| Leave out empty strings, slices and maps | omitempty |
Leave out an unset time.Time or nested struct | omitzero |
| Leave out a number or bool only when it was never set | A pointer with omitempty (or omitzero) |
| Always send the field, even when zero | No option |
Tell "missing" from null when decoding | A type with its own UnmarshalJSON |
| Code that behaves the same under v1 and v2 | omitzero 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.