Marshal and Unmarshal
json.Marshal turns a Go value into JSON bytes. json.Unmarshal fills a Go value from JSON bytes. Struct tags choose the JSON field names.
Email is missing from the first output because of omitempty and an empty string. Unmarshal needs a pointer (&back); passing the value itself returns an InvalidUnmarshalError.
Struct tags
The tag syntax is json:"name,option,option".
| Tag | Effect |
|---|---|
json:"user_id" | use user_id as the key |
json:"email,omitempty" | omit when false, 0, "", nil, or an empty slice or map |
json:",omitzero" (Go 1.24) | omit when the value is its zero value, or its IsZero() returns true |
json:"-" | never encode or decode this field |
json:"-," | use the literal key - |
json:"count,string" | encode a number or bool as a JSON string ("42") |
| no tag | the key is the Go field name, UserID |
The password never leaves the struct, the balance is quoted, and deleted_at disappears while created_at prints the zero time 0001-01-01T00:00:00Z. That difference is why omitzero was added: omitempty has never worked for structs, and it is a long-standing surprise with time.Time. On Go 1.23 and earlier, use a *time.Time with omitempty to get the same effect.
MarshalIndent(v, prefix, indent) produces readable output. Use it for config files and debugging; APIs usually send compact JSON.
Exported fields only
encoding/json uses reflection and can only see exported fields. This is the most common JSON bug in Go:
type point struct {
x, y int // lowercase: json.Marshal(point{1, 2}) gives {}
}
No error, no warning, just {} on the way out and fields left at zero on the way in. Capitalize the fields and add tags for lowercase keys.
How decoding matches fields
When unmarshaling into a struct:
- Keys are matched to the tag name, or to the field name, case-insensitively.
{"NAME": "x"}fills a field taggedjson:"name". - Keys with no matching field are ignored silently.
- Fields with no matching key keep their current value. Unmarshal does not reset them, so decoding into a struct that already has data merges into it.
- A type mismatch (a string where the struct has an
int) returns an*json.UnmarshalTypeError, but the other fields are still filled.
To reject unexpected keys, for example in a strict API or a config file with typos, use a Decoder with DisallowUnknownFields:
Unknown structure: map[string]any
When you do not know the shape in advance, decode into map[string]any or any. The mapping is fixed:
| JSON | Go |
|---|---|
| object | map[string]any |
| array | []any |
| string | string |
| number | float64 |
| true / false | bool |
| null | nil |
Two traps are visible in the output. Every number is float64, so m["stock"].(int) would fail. And integers above 2^53 lose precision as float64: the id 9007199254740993 comes back as ...992. UseNumber avoids both.
If you know part of the structure, decode that part into a struct and use json.RawMessage for the rest. It holds the raw bytes of a field so you can decode it later, once you know its type.
Slices, maps, pointers and nil
| Go value | JSON |
|---|---|
nil slice (var s []int) | null |
empty slice ([]int{}) | [] |
nil map | null |
nil pointer | null |
[]byte | a base64 string |
map[string]T | an object, keys sorted |
map[int]T | an object with the integer keys as strings |
The nil versus empty slice difference matters for API clients that expect an array. Initialize with []T{} or make([]T, 0) when the field must be [].
Use a pointer field (*int, *bool) when you need to tell "absent" from "zero" on input. After unmarshaling, a nil pointer means the key was missing or null; a pointer to 0 means the client sent 0.
Streams: Encoder and Decoder
json.Marshal and Unmarshal work on whole byte slices. For an io.Reader or io.Writer (an HTTP body, a file, stdin), use json.NewDecoder and json.NewEncoder. A Decoder can also read a sequence of JSON values one at a time:
Encoder.Encode writes a trailing newline after each value. By default both Marshal and Encoder escape <, > and & as \u003c, \u003e and \u0026, so the JSON is safe to embed in HTML. SetEscapeHTML(false) turns that off. In HTTP handlers, json.NewDecoder(r.Body).Decode(&v) and json.NewEncoder(w).Encode(v) are the usual pair.
Custom encoding
A type can control its own JSON by implementing json.Marshaler and json.Unmarshaler. A common case is an iota enum that should appear as a name, not a number:
MarshalJSON has a value receiver so it works for both values and pointers; UnmarshalJSON needs a pointer receiver because it modifies the value. Types that only need a string form can implement encoding.TextMarshaler (MarshalText) instead, which also makes them usable as map keys.
Common mistakes
- Lowercase field names. They are silently skipped.
- Passing a value to
Unmarshal. It needs a pointer. - Ignoring the error. Malformed JSON and type mismatches are reported only through it.
- Assuming numbers in
map[string]anyareint. They arefloat64. - Expecting
omitemptyto drop an empty struct or a zerotime.Time. Useomitzeroon Go 1.24, or a pointer. - Relying on field order in maps. Map keys are sorted on output; struct fields keep their declaration order.
Frequently Asked Questions
How do I convert a struct to JSON in Go?
Call json.Marshal(v), which returns []byte and an error. Only exported fields (names starting with a capital letter) are included. Use a struct tag such as json:"name" on a field to control its JSON key. For indented output use json.MarshalIndent(v, "", " ").
Why are my struct fields missing from the JSON output?
encoding/json only sees exported fields. A field named name (lowercase) is invisible to it, both when marshaling and when unmarshaling. Capitalize the field and set the JSON key with a tag such as json:"name".
What does omitempty do in Go JSON?
omitempty leaves a field out of the output when it holds an empty value: false, 0, "", a nil pointer or interface, or an empty slice or map. It does not treat a struct or time.Time as empty. Go 1.24 added omitzero, which omits any value that is its type's zero value (or whose IsZero() method returns true), including structs and time.Time.
How do I parse JSON with an unknown structure in Go?
Unmarshal into map[string]any (or any). Objects become map[string]any, arrays []any, strings string, booleans bool, and every number float64. Use type assertions to read the values, and Decoder.UseNumber if large integers must stay exact.