Menu

Golang JSON: Marshal, Unmarshal and Struct Tags

How to encode and decode JSON in Go with encoding/json: Marshal and Unmarshal, struct tags like omitempty and omitzero, pretty printing, decoding into map[string]any, rejecting unknown fields, and streaming with Decoder.

This page includes runnable editors - edit, run, and see output instantly.

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

TagEffect
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 tagthe 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 tagged json:"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:

JSONGo
objectmap[string]any
array[]any
stringstring
numberfloat64
true / falsebool
nullnil

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 valueJSON
nil slice (var s []int)null
empty slice ([]int{})[]
nil mapnull
nil pointernull
[]bytea base64 string
map[string]Tan object, keys sorted
map[int]Tan 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]any are int. They are float64.
  • Expecting omitempty to drop an empty struct or a zero time.Time. Use omitzero on 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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED