---
url: https://findutils.com/blog/go-json-omitempty-vs-omitzero-zero-values-pointers-and-null
title: "Go omitempty vs omitzero: Zero Values, Pointers and null in JSON"
description: "Why Go's omitempty drops a real 0 or false but keeps an empty time.Time, what omitzero (Go 1.24) does differently, when to use a pointer, and what changed with encoding/json/v2 in Go 1.27. Measured output."
category: developer
content_type: blog
cluster: json
locale: en
read_time: 9
status: published
author: "olgunozoktas"
published_at: 2026-10-24T15:08:00Z
excerpt: "omitempty leaves out false, 0, empty strings, nil pointers and empty slices, so a real zero vanishes from your JSON, while a zero time.Time is still written as 0001-01-01. omitzero, added in Go 1.24, omits the zero value of any type and respects IsZero. Here is what each tag writes, measured, plus how pointers, null and the Go 1.27 json/v2 defaults fit in."
tag_ids: ["go", "golang", "json", "api-design", "developer-tools"]
tags: ["Go", "Golang", "JSON", "API Design", "Developer Tools"]
primary_keyword: "go omitempty vs omitzero"
secondary_keywords: ["golang omitempty zero value", "go json omitzero", "golang json null pointer", "omitempty time.Time", "encoding/json/v2 omitempty", "golang distinguish missing and null json"]
tool_tag: "json-to-go-struct"
related_tool: "json-to-go-struct"
related_tools: ["json-to-go-struct", "json-formatter", "json-diff", "json-schema-validator", "http-request-builder", "json-to-typescript"]
updated_at: "2026-10-24T15:08:00Z"
og_image: "/images/content/blog/go-json-omitempty-vs-omitzero-zero-values-pointers-and-null-cover-20261024.webp"
image_alt: "Two conveyor lanes of value tiles pass through cyan and pink gates: the upper gate lets a zero and a false tile drop away, the lower gate keeps them and removes only a grey empty tile, on a dark background."
---

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:

```go
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:

```go
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:

| 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](/developers/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](/guides/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](/developers/json-diff/); to send a test request and look at the raw response, use the [HTTP Request Builder](/developers/http-request-builder/).

Need help with a project? The team behind FindUtils takes on [freelance work](https://www.upwork.com/freelancers/~010a5875eab37b4bd7), such as [a fast REST API or back-end service built in Go](https://www.upwork.com/services/product/development-it-a-fast-rest-api-or-back-end-service-built-in-go-2112349609254214411).

## 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.

## Related Reading

- [JSON to Go Struct guide](/guides/json-to-go-struct-guide/)
- [Unix timestamps: seconds vs milliseconds](/blog/unix-timestamp-seconds-vs-milliseconds/)
- [YAML vs JSON: which config format](/blog/yaml-vs-json-which-config-format/)
- [JSON Schema validation explained](/guides/json-schema-validation-explained/)

## Sources

- [Go standard library: package encoding/json](https://pkg.go.dev/encoding/json) (Marshal, Unmarshal, and "Migrating to v2")
- [Go standard library: package encoding/json/v2](https://pkg.go.dev/encoding/json/v2)
- [Go 1.24 Release Notes: encoding/json, omitzero](https://go.dev/doc/go1.24#encodingjsonpkgencodingjson)
- [Go 1.27 Release Notes: encoding/json/v2 and encoding/json/jsontext](https://go.dev/doc/go1.27)
- [RFC 8259, section 6: Numbers](https://datatracker.ietf.org/doc/html/rfc8259#section-6)
