Go JSON Marshaling Behavior: The Complete Reference

encoding/json has a lot of edge cases. Field tags, pointer indirection, nil vs empty, omitempty — it's easy to get surprised. This post is a structured reference covering every combination across the common types.

The matrix axes are:

  • Type: string, int, bool, float64, []string, map[string]string, struct
  • Tag: no tag, json:"f", json:"f,omitempty"
  • Receiver: value type vs pointer (*T)
  • Value: zero/nil/empty vs non-zero/non-empty

Each table includes output for both encoding/json (v1) and encoding/json/v2 (GOEXPERIMENT=jsonv2, Go 1.25+). Rows where they differ are marked .


Rules to know before reading the tables

Field naming. Without a tag, the field name is used as-is (F). With json:"f", it becomes f.

Nil pointer. A nil *T always marshals as null, regardless of omitempty — unless the field has omitempty, in which case it is omitted entirely.

omitempty definition (v1). A field is omitted when its value is the zero value for its type: "", 0, false, 0.0, nil pointer, nil/empty slice, nil/empty map. Structs are never considered empty — omitempty has no effect on value-type struct fields.

Non-nil pointer. A non-nil pointer is never omitted by omitempty — the pointer itself is non-nil, so it is not the zero value. The pointed-to value is marshaled normally.


string

PointerTagValuev1v2
nono tag""{"F":""}{"F":""}
nono tag"hello"{"F":"hello"}{"F":"hello"}
nojson:"f"""{"f":""}{"f":""}
nojson:"f""hello"{"f":"hello"}{"f":"hello"}
noomitempty""{}{}
noomitempty"hello"{"f":"hello"}{"f":"hello"}
yesno tagnil{"F":null}{"F":null}
yesno tagptr→""{"F":""}{"F":""}
yesno tagptr→"hello"{"F":"hello"}{"F":"hello"}
yesjson:"f"nil{"f":null}{"f":null}
yesjson:"f"ptr→""{"f":""}{"f":""}
yesjson:"f"ptr→"hello"{"f":"hello"}{"f":"hello"}
yesomitemptynil{}{}
yesomitemptyptr→""{"f":""}{}
yesomitemptyptr→"hello"{"f":"hello"}{"f":"hello"}

† v2 dereferences the pointer and omits the empty string behind it.


int

PointerTagValuev1v2
nono tag0{"F":0}{"F":0}
nono tag42{"F":42}{"F":42}
nojson:"f"0{"f":0}{"f":0}
nojson:"f"42{"f":42}{"f":42}
noomitempty0{}{"f":0}
noomitempty42{"f":42}{"f":42}
yesno tagnil{"F":null}{"F":null}
yesno tagptr→0{"F":0}{"F":0}
yesno tagptr→42{"F":42}{"F":42}
yesjson:"f"nil{"f":null}{"f":null}
yesjson:"f"ptr→0{"f":0}{"f":0}
yesjson:"f"ptr→42{"f":42}{"f":42}
yesomitemptynil{}{}
yesomitemptyptr→0{"f":0}{"f":0}
yesomitemptyptr→42{"f":42}{"f":42}

† v2 does not consider numeric zero to be "empty". Numbers are never omitted by v2's omitempty.


bool

PointerTagValuev1v2
nono tagfalse{"F":false}{"F":false}
nono tagtrue{"F":true}{"F":true}
nojson:"f"false{"f":false}{"f":false}
nojson:"f"true{"f":true}{"f":true}
noomitemptyfalse{}{"f":false}
noomitemptytrue{"f":true}{"f":true}
yesno tagnil{"F":null}{"F":null}
yesno tagptr→false{"F":false}{"F":false}
yesno tagptr→true{"F":true}{"F":true}
yesjson:"f"nil{"f":null}{"f":null}
yesjson:"f"ptr→false{"f":false}{"f":false}
yesjson:"f"ptr→true{"f":true}{"f":true}
yesomitemptynil{}{}
yesomitemptyptr→false{"f":false}{"f":false}
yesomitemptyptr→true{"f":true}{"f":true}

† v2 does not consider false to be "empty". Booleans are never omitted by v2's omitempty.


float64

PointerTagValuev1v2
nono tag0.0{"F":0}{"F":0}
nono tag3.14{"F":3.14}{"F":3.14}
nojson:"f"0.0{"f":0}{"f":0}
nojson:"f"3.14{"f":3.14}{"f":3.14}
noomitempty0.0{}{"f":0}
noomitempty3.14{"f":3.14}{"f":3.14}
yesno tagnil{"F":null}{"F":null}
yesno tagptr→0.0{"F":0}{"F":0}
yesno tagptr→3.14{"F":3.14}{"F":3.14}
yesjson:"f"nil{"f":null}{"f":null}
yesjson:"f"ptr→0.0{"f":0}{"f":0}
yesjson:"f"ptr→3.14{"f":3.14}{"f":3.14}
yesomitemptynil{}{}
yesomitemptyptr→0.0{"f":0}{"f":0}
yesomitemptyptr→3.14{"f":3.14}{"f":3.14}

† Same as int/bool: v2 never omits numeric zero.


[]string (slice)

Nil and empty slices behave differently. An empty slice ([]string{}) marshals as []. A nil slice marshals as null in v1, [] in v2. Both are omitted by omitempty.

PointerTagValuev1v2
nono tagnil{"F":null}{"F":[]}
nono tag[]{"F":[]}{"F":[]}
nono tag["a","b"]{"F":["a","b"]}{"F":["a","b"]}
nojson:"f"nil{"f":null}{"f":[]}
nojson:"f"[]{"f":[]}{"f":[]}
nojson:"f"["a","b"]{"f":["a","b"]}{"f":["a","b"]}
noomitemptynil{}{}
noomitempty[]{}{}
noomitempty["a","b"]{"f":["a","b"]}{"f":["a","b"]}
yesno tagnil ptr{"F":null}{"F":null}
yesno tagptr→nil{"F":null}{"F":[]}
yesno tagptr→[]{"F":[]}{"F":[]}
yesno tagptr→["a","b"]{"F":["a","b"]}{"F":["a","b"]}
yesjson:"f"nil ptr{"f":null}{"f":null}
yesjson:"f"ptr→[]{"f":[]}{"f":[]}
yesjson:"f"ptr→["a","b"]{"f":["a","b"]}{"f":["a","b"]}
yesomitemptynil ptr{}{}
yesomitemptyptr→nil{"f":null}{}
yesomitemptyptr→[]{"f":[]}{}
yesomitemptyptr→["a","b"]{"f":["a","b"]}{"f":["a","b"]}

† v2 unifies nil and empty slices (both → []), and dereferences pointers for omitempty checks.


map[string]string

Same nil vs empty distinction as slices.

PointerTagValuev1v2
nono tagnil{"F":null}{"F":{}}
nono tag{}{"F":{}}{"F":{}}
nono tag{"k":"v"}{"F":{"k":"v"}}{"F":{"k":"v"}}
nojson:"f"nil{"f":null}{"f":{}}
nojson:"f"{}{"f":{}}{"f":{}}
nojson:"f"{"k":"v"}{"f":{"k":"v"}}{"f":{"k":"v"}}
noomitemptynil{}{}
noomitempty{}{}{}
noomitempty{"k":"v"}{"f":{"k":"v"}}{"f":{"k":"v"}}
yesno tagnil ptr{"F":null}{"F":null}
yesno tagptr→nil{"F":null}{"F":{}}
yesno tagptr→{}{"F":{}}{"F":{}}
yesno tagptr→{"k":"v"}{"F":{"k":"v"}}{"F":{"k":"v"}}
yesjson:"f"nil ptr{"f":null}{"f":null}
yesjson:"f"ptr→{}{"f":{}}{"f":{}}
yesjson:"f"ptr→{"k":"v"}{"f":{"k":"v"}}{"f":{"k":"v"}}
yesomitemptynil ptr{}{}
yesomitemptyptr→nil{"f":null}{}
yesomitemptyptr→{}{"f":{}}{}
yesomitemptyptr→{"k":"v"}{"f":{"k":"v"}}{"f":{"k":"v"}}

† v2 unifies nil and empty maps (both → {}), and dereferences pointers for omitempty checks.


struct

Structs are never "empty". omitempty on a value-type struct field is a no-op in both versions. Only a nil pointer to a struct is omitted.

PointerTagValuev1v2
nono tag{X:0}{"F":{"x":0}}{"F":{"x":0}}
nono tag{X:7}{"F":{"x":7}}{"F":{"x":7}}
nojson:"f"{X:0}{"f":{"x":0}}{"f":{"x":0}}
nojson:"f"{X:7}{"f":{"x":7}}{"f":{"x":7}}
noomitempty{X:0}{"f":{"x":0}}{"f":{"x":0}}
noomitempty{X:7}{"f":{"x":7}}{"f":{"x":7}}
yesno tagnil{"F":null}{"F":null}
yesno tagptr→{X:0}{"F":{"x":0}}{"F":{"x":0}}
yesno tagptr→{X:7}{"F":{"x":7}}{"F":{"x":7}}
yesjson:"f"nil{"f":null}{"f":null}
yesjson:"f"ptr→{X:0}{"f":{"x":0}}{"f":{"x":0}}
yesjson:"f"ptr→{X:7}{"f":{"x":7}}{"f":{"x":7}}
yesomitemptynil{}{}
yesomitemptyptr→{X:0}{"f":{"x":0}}{"f":{"x":0}}
yesomitemptyptr→{X:7}{"f":{"x":7}}{"f":{"x":7}}

No differences between v1 and v2 for structs.


Quick-reference: omitempty behavior by type

TypeZero valuev1 omits?v2 omits?
string""yesyes
int / float640 / 0.0yesno
boolfalseyesno
[]Tnilyesyes
[]T[] (empty)yesyes
map[K]Vnilyesyes
map[K]V{} (empty)yesyes
struct{...} (any)nono
*Tnilyesyes
*Tnon-nil ptr to zero valuenoyes (v2 dereferences)
*Tnon-nil ptr to non-zero valuenono
PostgreSQL drivers for streaming in Node.js