JSON in Go: encoding/json Marshal, Unmarshal, Struct Tags & Streaming

Last updated:

Go's standard library encoding/json package serializes Go values to JSON with json.Marshal(v) — returning []byte and an error — and deserializes JSON bytes into Go values with json.Unmarshal(data, &target), which always requires a pointer target. Struct field tags control JSON key names: {"`json:"user_name"`"} maps the Go field UserName to "user_name" in JSON, and adding ,omitempty skips the field entirely when it holds a zero value (empty string, 0, nil, false). Unexported fields (lowercase first letter) are silently ignored by both Marshal and Unmarshal. For HTTP handlers, json.NewDecoder(r.Body).Decode(&v) streams JSON directly from the request body without buffering all bytes into memory first — the correct pattern for large payloads. json.RawMessage holds undecoded JSON bytes, enabling delayed or conditional parsing of nested objects. This guide covers json.Marshal/json.Unmarshal, struct tags, omitempty, json.Decoder/json.Encoder for streaming, json.RawMessage, custom MarshalJSON/UnmarshalJSON methods, and handling unknown fields with map[string]interface{}.

json.Marshal: Serializing Go Structs and Maps to JSON

json.Marshal(v) accepts any Go value and returns ([]byte, error). Structs are serialized as JSON objects — each exported field becomes a key. By default the key name matches the field name exactly; struct tags override this. Maps with string keys serialize as JSON objects. Slices serialize as JSON arrays. Nil pointers serialize as null. Calling json.MarshalIndent(v, "", " ") produces human-readable JSON with two-space indentation.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// ── Struct serialization ───────────────────────────────────────
type User struct {
	ID       int    `json:"id"`
	Name     string `json:"name"`
	Email    string `json:"email"`
	Password string `json:"-"`          // always excluded
	Bio      string `json:"bio,omitempty"` // omitted when empty
}

func main() {
	u := User{
		ID:       42,
		Name:     "Alice",
		Email:    "alice@example.com",
		Password: "secret",        // never appears in JSON
		Bio:      "",              // omitted because omitempty
	}

	data, err := json.Marshal(u)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data))
	// {"id":42,"name":"Alice","email":"alice@example.com"}

	// ── Pretty print ───────────────────────────────────────────
	pretty, _ := json.MarshalIndent(u, "", "  ")
	fmt.Println(string(pretty))
	// {
	//   "id": 42,
	//   "name": "Alice",
	//   "email": "alice@example.com"
	// }

	// ── Map serialization ──────────────────────────────────────
	m := map[string]interface{}{
		"status":  "ok",
		"count":   100,
		"enabled": true,
	}
	mapData, _ := json.Marshal(m)
	fmt.Println(string(mapData))
	// {"count":100,"enabled":true,"status":"ok"}
	// Note: map keys are sorted alphabetically in the output

	// ── Slice serialization ────────────────────────────────────
	scores := []int{95, 87, 72}
	scoresData, _ := json.Marshal(scores)
	fmt.Println(string(scoresData))
	// [95,87,72]

	// ── Nil pointer → JSON null ────────────────────────────────
	var ptr *User
	nullData, _ := json.Marshal(ptr)
	fmt.Println(string(nullData))
	// null
}

json.Marshal returns a non-nil error for Go types that cannot be represented in JSON: channels, functions, and cyclic data structures. For float64 values, NaN and ±Inf also cause an error (UnsupportedValueError). Map keys must be strings, integers, or types that implement encoding.TextMarshaler. The marshaler walks the entire value tree before returning, so very large structures will hold all data in memory during the call — for large streaming output, prefer json.NewEncoder.

json.Unmarshal: Parsing JSON into Go Structs

json.Unmarshal(data []byte, v interface{}) parses JSON and stores the result in the value pointed to by v. The second argument must always be a pointer. JSON object keys are matched to struct fields case-insensitively by default; a json: tag takes precedence. Unknown JSON keys are silently ignored — enabling forward-compatible reading of evolving APIs.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Address struct {
	Street string `json:"street"`
	City   string `json:"city"`
	Zip    string `json:"zip"`
}

type Person struct {
	ID      int     `json:"id"`
	Name    string  `json:"name"`
	Age     int     `json:"age,omitempty"`
	Address Address `json:"address"`
	Tags    []string `json:"tags"`
}

func main() {
	data := []byte(`{
		"id": 1,
		"name": "Bob",
		"age": 30,
		"address": {"street": "123 Main St", "city": "Austin", "zip": "78701"},
		"tags": ["go", "backend"],
		"unknownField": "ignored silently"
	}`)

	var p Person
	if err := json.Unmarshal(data, &p); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+v
", p)
	// {ID:1 Name:Bob Age:30 Address:{Street:123 Main St City:Austin Zip:78701} Tags:[go backend]}

	// ── Error: passing non-pointer ─────────────────────────────
	var bad Person
	err := json.Unmarshal(data, bad) // missing &
	fmt.Println(err)
	// json: Unmarshal(non-pointer main.Person)

	// ── Partial update: unmarshal into existing struct ─────────
	existing := Person{ID: 1, Name: "Bob", Tags: []string{"existing"}}
	patch := []byte(`{"name":"Robert","tags":["go","backend","updated"]}`)
	_ = json.Unmarshal(patch, &existing) // merges into existing
	fmt.Println(existing.Name) // Robert
	fmt.Println(existing.ID)   // 1 (unchanged)

	// ── Reject unknown fields ──────────────────────────────────
	dec := json.NewDecoder(strings.NewReader(string(data)))
	dec.DisallowUnknownFields()
	var strict Person
	if err := dec.Decode(&strict); err != nil {
		fmt.Println(err)
		// json: unknown field "unknownField"
	}
}

When Unmarshal cannot assign a JSON value to a Go field — for example, a JSON string arriving in an int field — it returns a *json.UnmarshalTypeError containing the field name, expected Go type, and the received JSON type. *json.SyntaxError is returned for malformed JSON, carrying an Offset byte position. Always check the concrete error type when you need to distinguish user input errors from structural API mismatches.

Struct Tags: json key names, omitempty, and - (ignore)

Go struct tags are string literals in backtick quotes placed after the field type. The encoding/json package reads the json key from the struct tag. The tag format is {"`json:"name[,options]"`"} where options are comma-separated. The three most important options: custom key name, omitempty, and - (always exclude).

package main

import (
	"encoding/json"
	"fmt"
)

type Product struct {
	// ── Custom key name ────────────────────────────────────────
	ProductID   int     `json:"product_id"`   // exported as "product_id"
	ProductName string  `json:"name"`         // exported as "name"

	// ── omitempty: skip field when zero value ──────────────────
	Description string  `json:"description,omitempty"` // omitted if ""
	Price       float64 `json:"price,omitempty"`       // omitted if 0.0
	Stock       int     `json:"stock,omitempty"`       // omitted if 0
	Available   bool    `json:"available,omitempty"`   // omitted if false
	Tags        []string `json:"tags,omitempty"`       // omitted if nil/empty

	// ── Pointer + omitempty: omit nested struct when nil ───────
	Metadata    *map[string]string `json:"metadata,omitempty"` // nil → omitted

	// ── "-": always exclude from JSON ─────────────────────────
	InternalRef string  `json:"-"`  // never appears in marshal/unmarshal

	// ── No tag: uses field name as-is (case-sensitive) ─────────
	SKU         string  // exported as "SKU"

	// ── Unexported: always ignored ────────────────────────────
	internalCost float64 // silently ignored
}

func main() {
	p := Product{
		ProductID:   101,
		ProductName: "Widget",
		Price:       9.99,
		SKU:         "WGT-101",
		// Description, Stock, Available, Tags, Metadata all zero/nil → omitted
	}

	data, _ := json.Marshal(p)
	fmt.Println(string(data))
	// {"product_id":101,"name":"Widget","price":9.99,"SKU":"WGT-101"}

	// ── omitempty on struct (no effect — structs are never zero) ─
	type Nested struct {
		X int `json:"x"`
	}
	type Outer struct {
		A Nested  `json:"a,omitempty"` // ← omitempty has NO effect on struct!
		B *Nested `json:"b,omitempty"` // ← pointer: nil is omitted
	}
	o1 := Outer{A: Nested{X: 0}} // A is included even though X == 0
	o2 := Outer{B: nil}          // B is omitted
	d1, _ := json.Marshal(o1)
	d2, _ := json.Marshal(o2)
	fmt.Println(string(d1)) // {"a":{"x":0}}
	fmt.Println(string(d2)) // {}

	// ── json:"-," (comma after dash) keeps the field with key "-" ─
	type Special struct {
		DashKey string `json:"-,"` // key is literally "-"
	}
	s := Special{DashKey: "value"}
	sd, _ := json.Marshal(s)
	fmt.Println(string(sd)) // {"-":"value"}
}

A common mistake: omitempty does not work on struct-typed fields (only on pointer-to-struct). If you want a nested struct to be omitted when empty, use a pointer field. Another subtle point: if you specify only omitempty without a custom name, the field still uses its Go name in JSON — write {"`json:",omitempty"`"} (comma before omitempty with empty name string). Both Marshal and Unmarshal honor the json:"-" tag, meaning a field excluded from output is also never populated during parsing.

Streaming JSON with json.Decoder and json.Encoder

json.NewDecoder(r io.Reader) and json.NewEncoder(w io.Writer) work directly on streams. The decoder is the correct tool for HTTP request bodies — it avoids reading the entire body into memory. The encoder writes JSON directly to an io.Writer such as http.ResponseWriter. Both support multiple sequential JSON values on the same stream.

package main

import (
	"encoding/json"
	"net/http"
	"strings"
	"fmt"
	"log"
)

type Order struct {
	ID     int     `json:"id"`
	Item   string  `json:"item"`
	Amount float64 `json:"amount"`
}

// ── HTTP handler: decode request body ─────────────────────────
func createOrderHandler(w http.ResponseWriter, r *http.Request) {
	var order Order

	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields() // reject unexpected keys

	if err := dec.Decode(&order); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	// Process order...

	// ── Encode response directly to ResponseWriter ─────────────
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	enc := json.NewEncoder(w)
	enc.SetIndent("", "  ") // optional pretty output
	if err := enc.Encode(order); err != nil {
		log.Printf("encode error: %v", err)
	}
}

// ── Stream multiple JSON values from a reader ──────────────────
func readJSONStream(input string) {
	dec := json.NewDecoder(strings.NewReader(input))

	for dec.More() {
		var order Order
		if err := dec.Decode(&order); err != nil {
			log.Printf("decode error: %v", err)
			break
		}
		fmt.Printf("Order #%d: %s — $%.2f
", order.ID, order.Item, order.Amount)
	}
}

// ── Stream a large JSON array element-by-element ───────────────
func streamJSONArray(r *strings.Reader) error {
	dec := json.NewDecoder(r)

	// Read opening bracket
	tok, err := dec.Token()
	if err != nil {
		return err
	}
	if delim, ok := tok.(json.Delim); !ok || delim != '[' {
		return fmt.Errorf("expected '[', got %v", tok)
	}

	// Read each element without loading the whole array
	for dec.More() {
		var order Order
		if err := dec.Decode(&order); err != nil {
			return err
		}
		fmt.Printf("streaming: %+v
", order)
	}

	// Read closing bracket
	if _, err := dec.Token(); err != nil {
		return err
	}
	return nil
}

// ── UseNumber: preserve integer precision for large IDs ────────
func parseWithNumber(data []byte) {
	dec := json.NewDecoder(strings.NewReader(string(data)))
	dec.UseNumber() // numbers decoded as json.Number (string), not float64

	var m map[string]interface{}
	dec.Decode(&m)

	id := m["id"].(json.Number)
	fmt.Println(id.String())    // "9223372036854775807" — exact
	fmt.Println(id.Int64())     // returns (int64, error)
}

func main() {
	ndjson := `{"id":1,"item":"Widget","amount":9.99}
{"id":2,"item":"Gadget","amount":24.99}
{"id":3,"item":"Doohickey","amount":4.99}`

	readJSONStream(ndjson)
	// Order #1: Widget — $9.99
	// Order #2: Gadget — $24.99
	// Order #3: Doohickey — $4.99
}

json.NewDecoder buffers internally — calling Decode may read more bytes from the reader than a single JSON value requires. This means the reader's position after Decode is not exactly at the end of the first JSON value. For interleaved protocols where you must read non-JSON data after a JSON value, use json.NewDecoder with dec.Buffered() to access the remaining buffered bytes. For most HTTP use cases this is not a concern. json.NewEncoder by default does not set Content-Length — the HTTP server uses chunked transfer encoding. Call json.Marshal and write the bytes manually if you need a known Content-Length.

json.RawMessage: Deferred and Conditional JSON Parsing

json.RawMessage is simply []byte with MarshalJSON/UnmarshalJSON methods that copy the raw bytes. When used as a struct field type, Unmarshal stores the raw JSON token (object, array, string, number) without parsing it, and Marshal emits the bytes verbatim. This enables deferred parsing — unmarshal the envelope first, then decide how to parse the payload based on a discriminant field.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// ── Deferred parsing: parse envelope first, payload later ──────
type Event struct {
	Type    string          `json:"type"`
	Payload json.RawMessage `json:"payload"` // raw bytes, not yet decoded
}

type ClickPayload struct {
	X, Y   int    `json:"x,omitempty"`
	Button string `json:"button"`
}

type KeyPayload struct {
	Key  string `json:"key"`
	Code string `json:"code"`
}

func processEvent(data []byte) error {
	var event Event
	if err := json.Unmarshal(data, &event); err != nil {
		return err
	}

	switch event.Type {
	case "click":
		var p ClickPayload
		if err := json.Unmarshal(event.Payload, &p); err != nil {
			return err
		}
		fmt.Printf("Click at (%d, %d) button=%s
", p.X, p.Y, p.Button)
	case "keypress":
		var p KeyPayload
		if err := json.Unmarshal(event.Payload, &p); err != nil {
			return err
		}
		fmt.Printf("Key: %s (%s)
", p.Key, p.Code)
	default:
		fmt.Printf("Unknown event type %q, payload: %s
", event.Type, event.Payload)
	}
	return nil
}

// ── Preserving raw JSON when re-marshaling ─────────────────────
type Response struct {
	Status int             `json:"status"`
	Data   json.RawMessage `json:"data"` // pass-through without re-encoding
}

// ── json.RawMessage as a dynamic container ─────────────────────
func buildDynamic(data map[string]json.RawMessage) ([]byte, error) {
	return json.Marshal(data)
}

func main() {
	clickEvent := []byte(`{"type":"click","payload":{"x":100,"y":200,"button":"left"}}`)
	keyEvent   := []byte(`{"type":"keypress","payload":{"key":"Enter","code":"Enter"}}`)

	processEvent(clickEvent) // Click at (100, 200) button=left
	processEvent(keyEvent)   // Key: Enter (Enter)

	// ── Wrap pre-encoded JSON without double-encoding ──────────
	preEncoded := json.RawMessage(`{"nested":"already encoded","count":42}`)
	resp := Response{Status: 200, Data: preEncoded}
	out, _ := json.Marshal(resp)
	fmt.Println(string(out))
	// {"status":200,"data":{"nested":"already encoded","count":42}}

	// ── Dynamic map of raw values ─────────────────────────────
	dynamic := map[string]json.RawMessage{
		"string": json.RawMessage(`"hello"`),
		"number": json.RawMessage(`42`),
		"array":  json.RawMessage(`[1,2,3]`),
	}
	dynOut, _ := buildDynamic(dynamic)
	fmt.Println(string(dynOut))
	// {"array":[1,2,3],"number":42,"string":"hello"}
	_ = log.Writer
}

json.RawMessage is also useful for API proxies — receive a JSON payload, inspect one field to determine routing, then forward the json.RawMessage to a downstream handler without re-encoding. Because RawMessage stores verbatim bytes, it preserves whitespace and key ordering in the original — unlike re-marshaling which normalizes both. A nil json.RawMessage marshals to null; an empty json.RawMessage{} marshals to an empty byte sequence and may cause parsing errors in the caller — always ensure the bytes are valid JSON before using them.

Custom MarshalJSON and UnmarshalJSON Methods

Implement json.Marshaler (MarshalJSON() ([]byte, error)) or json.Unmarshaler (UnmarshalJSON([]byte) error) to fully control how a type is serialized or parsed. The encoding/json package checks for these interfaces before using its default reflection-based logic.

package main

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"
)

// ── Custom time serialization: Unix timestamp instead of RFC 3339 ─
type UnixTime struct {
	time.Time
}

func (t UnixTime) MarshalJSON() ([]byte, error) {
	return json.Marshal(t.Time.Unix()) // serialize as integer seconds
}

func (t *UnixTime) UnmarshalJSON(data []byte) error {
	var ts int64
	if err := json.Unmarshal(data, &ts); err != nil {
		return err
	}
	t.Time = time.Unix(ts, 0).UTC()
	return nil
}

// ── Custom string enum ─────────────────────────────────────────
type Status int

const (
	StatusPending Status = iota
	StatusActive
	StatusClosed
)

var statusNames = map[Status]string{
	StatusPending: "pending",
	StatusActive:  "active",
	StatusClosed:  "closed",
}

var statusValues = map[string]Status{
	"pending": StatusPending,
	"active":  StatusActive,
	"closed":  StatusClosed,
}

func (s Status) MarshalJSON() ([]byte, error) {
	name, ok := statusNames[s]
	if !ok {
		return nil, fmt.Errorf("unknown status: %d", int(s))
	}
	return json.Marshal(name)
}

func (s *Status) UnmarshalJSON(data []byte) error {
	var name string
	if err := json.Unmarshal(data, &name); err != nil {
		return err
	}
	val, ok := statusValues[strings.ToLower(name)]
	if !ok {
		return fmt.Errorf("invalid status: %q", name)
	}
	*s = val
	return nil
}

// ── Adding computed fields without changing the struct ─────────
type Circle struct {
	Radius float64 `json:"radius"`
}

func (c Circle) MarshalJSON() ([]byte, error) {
	// Use an alias to avoid infinite recursion
	type Alias Circle
	return json.Marshal(struct {
		Alias
		Area float64 `json:"area"`
	}{
		Alias: Alias(c),
		Area:  3.14159 * c.Radius * c.Radius,
	})
}

func main() {
	// UnixTime demo
	type Event struct {
		Name      string   `json:"name"`
		CreatedAt UnixTime `json:"created_at"`
	}
	e := Event{Name: "launch", CreatedAt: UnixTime{time.Unix(1748390400, 0)}}
	data, _ := json.Marshal(e)
	fmt.Println(string(data))
	// {"name":"launch","created_at":1748390400}

	var e2 Event
	json.Unmarshal([]byte(`{"name":"launch","created_at":1748390400}`), &e2)
	fmt.Println(e2.CreatedAt.Format(time.RFC3339)) // 2025-05-28T00:00:00Z

	// Status enum demo
	type Order struct {
		ID     int    `json:"id"`
		Status Status `json:"status"`
	}
	o := Order{ID: 1, Status: StatusActive}
	od, _ := json.Marshal(o)
	fmt.Println(string(od)) // {"id":1,"status":"active"}

	var o2 Order
	json.Unmarshal([]byte(`{"id":2,"status":"closed"}`), &o2)
	fmt.Println(o2.Status == StatusClosed) // true

	// Circle with computed area
	c := Circle{Radius: 5}
	cd, _ := json.Marshal(c)
	fmt.Println(string(cd)) // {"radius":5,"area":78.53975}
}

The alias pattern in Circle.MarshalJSON (type Alias Circle) is essential: without it, calling json.Marshal(c) inside MarshalJSON would recurse infinitely because Circle still satisfies the json.Marshaler interface. The alias type does not inherit the methods of Circle, so json.Marshal(Alias(c)) falls back to default struct reflection. This same technique applies whenever you need to extend default marshaling with extra fields.

Handling Unknown JSON Fields with map[string]interface and json.Number

When the JSON structure is not known at compile time — plugin data, arbitrary API responses, dynamic configuration — unmarshal into map[string]interface{} or map[string]json.RawMessage. By default, encoding/json uses float64 for all JSON numbers in interface{} values, which can lose precision for large integers. Use dec.UseNumber() to get json.Number instead, which is a string that provides .Int64(), .Float64(), and .String() methods.

package main

import (
	"encoding/json"
	"fmt"
	"strings"
)

func main() {
	// ── Dynamic JSON: map[string]interface{} ───────────────────
	data := []byte(`{
		"name": "Alice",
		"age":  30,
		"active": true,
		"score": 98.6,
		"tags": ["go", "backend"],
		"address": {"city": "Austin"}
	}`)

	var m map[string]interface{}
	json.Unmarshal(data, &m)

	fmt.Println(m["name"])   // Alice
	fmt.Println(m["age"])    // 30 — but type is float64!
	fmt.Printf("%T
", m["age"]) // float64

	// Type assert to use the value
	if age, ok := m["age"].(float64); ok {
		fmt.Println(int(age)) // 30
	}

	// Nested object is also map[string]interface{}
	if addr, ok := m["address"].(map[string]interface{}); ok {
		fmt.Println(addr["city"]) // Austin
	}

	// Array is []interface{}
	if tags, ok := m["tags"].([]interface{}); ok {
		for _, t := range tags {
			fmt.Println(t.(string))
		}
	}

	// ── UseNumber: preserve integer precision ──────────────────
	bigData := []byte(`{"id": 9223372036854775807, "value": 3.14159}`)
	dec := json.NewDecoder(strings.NewReader(string(bigData)))
	dec.UseNumber()

	var n map[string]interface{}
	dec.Decode(&n)

	id := n["id"].(json.Number)
	fmt.Println(id.String())      // "9223372036854775807"
	i64, _ := id.Int64()
	fmt.Println(i64)             // 9223372036854775807 — exact

	val := n["value"].(json.Number)
	f64, _ := val.Float64()
	fmt.Println(f64)             // 3.14159

	// ── map[string]json.RawMessage: selective decoding ─────────
	type PartialParse struct {
		Type string          `json:"type"`
		Data json.RawMessage `json:"data"`  // raw — parsed later
		Meta map[string]json.RawMessage `json:"meta,omitempty"`
	}

	pp := []byte(`{"type":"order","data":{"id":1,"total":99.9},"meta":{"env":"prod"}}`)
	var partial PartialParse
	json.Unmarshal(pp, &partial)
	fmt.Println(partial.Type)         // order
	fmt.Println(string(partial.Data)) // {"id":1,"total":99.9}

	// ── Merging known + unknown fields ─────────────────────────
	type KnownWithExtras struct {
		ID   int                        `json:"id"`
		Name string                     `json:"name"`
		// Unknown keys captured here:
		Extra map[string]json.RawMessage `json:"-"`
	}
	// Implement custom UnmarshalJSON to capture extras
	// (not shown for brevity — use a two-pass approach:
	//  first unmarshal into the struct, then unmarshal
	//  into a map and delete the known keys)
	_ = partial
}

The map[string]interface{} approach is flexible but requires type assertions everywhere, which can cause panics if the JSON shape changes unexpectedly. For production code that handles dynamic JSON, prefer map[string]json.RawMessage — you get the raw bytes for each key and can unmarshal them individually with full error handling. A common real-world pattern: unmarshal the top-level object into map[string]json.RawMessage, then unmarshal each well-known key into the appropriate typed struct, leaving unknown keys as raw bytes for logging or forwarding.

Key Terms

json.Marshal
The primary serialization function in encoding/json. json.Marshal(v interface{}) ([]byte, error) converts any Go value to its JSON byte representation. It uses reflection to inspect struct fields and their tags. Exported fields with json: tags use the tag name; otherwise the field name is used as-is. Nil pointers serialize to null. Cyclic structures, channels, and functions return an error. For streaming output to a writer, prefer json.NewEncoder(w).Encode(v).
json.Unmarshal
The primary deserialization function. json.Unmarshal(data []byte, v interface{}) error parses JSON bytes and populates the value pointed to by v. The second argument must be a non-nil pointer; passing a non-pointer returns InvalidUnmarshalError immediately. Unknown JSON keys are silently ignored by default. Type mismatches (JSON string into Go int field) return *UnmarshalTypeError. For streaming from a reader, use json.NewDecoder.
struct tag
A compile-time string annotation in backticks placed after a struct field declaration. The encoding/json package reads the json: tag key. Format: {"`json:"name,options"`"}. The name overrides the JSON key. Options: omitempty (skip when zero), - (always exclude; use -, to use literal key "-"). Tags are accessed at runtime via the reflect package. Misspelled tag keys (e.g., JSON: instead of json:) are silently ignored — the field uses its Go name.
json.RawMessage
A []byte type alias that implements both json.Marshaler and json.Unmarshaler by storing and emitting raw JSON bytes verbatim. When a struct field has type json.RawMessage, Unmarshal stores the exact bytes of that JSON value without parsing them further. Marshal emits those bytes directly into the output stream. Useful for: deferred conditional parsing (parse a type discriminant first, then parse the payload), pass-through proxies that forward JSON without re-encoding, and building dynamic JSON documents from pre-serialized fragments.
json.Decoder
A streaming JSON decoder created with json.NewDecoder(r io.Reader). Reads JSON from any io.Reader — HTTP bodies, files, network connections — without buffering the entire source in memory. The primary method is Decode(v interface{}) error, which reads the next complete JSON value and unmarshals it. More() returns true if another value remains. Token() reads one JSON token at a time for fine-grained control. Configuration methods: DisallowUnknownFields() rejects extra keys; UseNumber() decodes numbers as json.Number instead of float64.
omitempty
A struct tag option that instructs json.Marshal to skip the field when it holds the zero value for its type. Zero values: false (bool), 0 (int, float), "" (string), nil (pointer, interface, map, slice, channel). Struct-typed fields are never considered zero regardless of their contents — use a pointer to the struct to get omitempty behavior for nested objects. omitempty has no effect on Unmarshal; it is a serialization-only option. The option must follow a comma after the key name in the tag: {"`json:"key,omitempty"`"}.

FAQ

How do I serialize a Go struct to JSON?

Call json.Marshal(v) with any Go value — struct, map, slice, or primitive. It returns ([]byte, error). On success the error is nil and the byte slice contains valid JSON. For a struct, only exported fields (starting with an uppercase letter) are included. Add struct tags to control the JSON key name: {"`json:"user_name"`"} maps the field to "user_name" in the output. Add ,omitempty to skip zero-valued fields: {"`json:"bio,omitempty"`"} omits the field when the string is empty. Use {"`json:"-"`"} to always exclude a field. For HTTP responses, wrap json.Marshal in json.NewEncoder(w).Encode(v) to write directly to an http.ResponseWriter without allocating an intermediate byte slice. json.Marshal handles nested structs, embedded structs, pointers (nil pointer marshals to JSON null), and all primitive types correctly. Cyclic data structures return a UnsupportedValueError. The encoding/json package is part of the Go standard library — no external dependency is needed.

How do I parse JSON into a Go struct?

Call json.Unmarshal(data, &target) where data is []byte containing valid JSON and &target is a pointer to the Go value to populate. Passing a non-pointer returns json.InvalidUnmarshalError immediately. json.Unmarshal matches JSON keys to struct fields case-insensitively, then by json struct tag, then by exact field name. Unknown JSON keys are silently ignored by default — this is intentional for forward compatibility. If you want to reject unknown keys, use a decoder: d := json.NewDecoder(...); d.DisallowUnknownFields(); d.Decode(&v). For JSON arrays, unmarshal into a Go slice. For JSON objects with dynamic keys, unmarshal into map[string]interface{} or map[string]json.RawMessage. Always check the returned error — json.Unmarshal returns *json.SyntaxError for malformed JSON and *json.UnmarshalTypeError when a JSON value cannot be stored in the target Go type. For HTTP request bodies, prefer json.NewDecoder(r.Body).Decode(&v).

What does omitempty do in Go JSON struct tags?

The omitempty option in a struct tag tells json.Marshal to skip the field when it holds its zero value. The zero values are: false for bool, 0 for any numeric type (int, float64, etc.), "" for string, nil for pointers, interfaces, maps, slices, and channels. A struct value is never zero — a struct field is always included unless you use a pointer. Add omitempty with a comma after the JSON key name: {"`json:"score,omitempty"`"} omits the "score" key when score == 0. This is commonly used for optional API response fields and reduces payload size. A critical subtlety: omitempty has no effect on struct-typed fields — a struct field is always serialized even if all its subfields are zero. To omit an optional nested struct, use a pointer: *Address `{"`json:\"address,omitempty\"`"}` — nil pointer marshals to nothing, non-nil pointer marshals to the nested object. omitempty does not affect Unmarshal at all; it is a serialization-only directive. Approximately 60–70% of real-world Go JSON structs use omitempty on at least one field.

How do I handle unknown JSON fields in Go?

By default, json.Unmarshal silently discards JSON keys that have no matching struct field — safe for forward compatibility with evolving APIs. To capture unknown fields instead of discarding them, add a map[string]json.RawMessage field and implement custom UnmarshalJSON. A simpler alternative: unmarshal into map[string]interface{} first, then extract known keys with type assertions. To reject unknown fields entirely (strict parsing), use a decoder: d := json.NewDecoder(r.Body); d.DisallowUnknownFields(); err := d.Decode(&v) — this returns an error with the message "unknown field "fieldname"" for any key not present in the target struct. DisallowUnknownFields() has been available since Go 1.10. For capturing but not immediately parsing extra keys, unmarshal into a map[string]json.RawMessage and process known keys explicitly, leaving others as raw bytes for logging or forwarding.

What is the difference between json.Unmarshal and json.NewDecoder in Go?

json.Unmarshal(data, &v) operates on a []byte that is already fully in memory — you must read the entire input first. json.NewDecoder(r).Decode(&v) streams JSON from any io.Reader without buffering the entire source, making it the correct choice for HTTP request bodies (r.Body), files, and network sockets where you do not want to allocate a full copy. For HTTP handlers, always use json.NewDecoder(r.Body).Decode(&v) — it avoids an extra io.ReadAll() allocation and automatically respects any body size limit set on the server. json.NewDecoder also supports multiple sequential JSON values in a single stream (call Decode in a loop), which json.Unmarshal cannot do. The decoder provides DisallowUnknownFields() and UseNumber() — the latter parses numbers as json.Number instead of float64, preserving full precision for integers larger than 2⁵₃ (9,007,199,254,740,992). For in-memory []byte data, json.Unmarshal is simpler; for any io.Reader, use json.NewDecoder.

How do I write a custom JSON marshaler in Go?

Implement the json.Marshaler interface by adding a MarshalJSON() ([]byte, error) method to your type. encoding/json calls MarshalJSON automatically during json.Marshal if the type implements it. Return the JSON bytes directly — you are responsible for producing valid JSON. For the corresponding input side, implement json.Unmarshaler: UnmarshalJSON(data []byte) error, receiving raw JSON bytes to parse however you want. Common use cases: formatting time.Time as Unix timestamps instead of RFC 3339 (the default), serializing enum types as strings, and adding computed fields to JSON output. Use an alias type (type Alias T) inside MarshalJSON to avoid infinite recursion when calling json.Marshal on a modified version of the same struct. Custom marshalers add approximately 10–15% overhead compared to struct tag-based marshaling, so use them only when struct tags are insufficient.

How do I parse a JSON array in Go?

Declare a Go slice of the element type and unmarshal into a pointer to it. For example, to parse [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}], declare var users []User and call json.Unmarshal(data, &users). The slice is allocated by json.Unmarshal to the exact length needed. For a JSON array of strings: var names []string. For a heterogeneous JSON array: var items []interface{}. For streaming large JSON arrays element-by-element without loading the entire array into memory, use json.NewDecoder and the token-based pattern: dec.Token() reads the opening [ delimiter, then for dec.More() { dec.Decode(&item) } reads each element one at a time, and dec.Token() reads the closing ]. This streaming pattern processes arrays of any size in O(1) memory — critical for large datasets. json.Unmarshal appends to nil slices; if you unmarshal into a non-nil slice, existing elements are overwritten index-by-index and extra elements are appended. Always check the error on each Decode call in streaming loops.

Why are my Go struct fields missing from JSON output?

The most common cause is unexported fields — Go field names starting with a lowercase letter are package-private and silently ignored by encoding/json during both Marshal and Unmarshal. Only fields with an uppercase first letter (exported fields) participate in JSON serialization. If your field is exported but still missing: it may have a {"`json:"-"`"} tag explicitly excluding it; it may have omitempty and currently holds its zero value (empty string, 0, false, nil pointer); or the field type may be a channel, func, or complex number which json.Marshal cannot encode and returns UnsupportedTypeError. Another subtle case: an embedded struct with unexported fields — embedded unexported structs are completely invisible to encoding/json. To expose fields from an unexported embedded type, either promote them to the outer struct or implement custom MarshalJSON. Verify by adding a {"`json:"fieldname"`"} tag explicitly, which overrides all name-matching logic and makes the field always present in the output (unless omitempty applies and the value is zero).

Generate Go structs from JSON automatically

Paste any JSON object into Jsonic's JSON-to-Go generator to get a complete struct with proper field tags instantly.

Open JSON to Go Generator

Further reading and primary sources