JSON Marshal and Unmarshal in Go

Last updated:

Go's standard library encoding/json package provides everything you need to work with JSON: marshaling (Go value to JSON bytes), unmarshaling (JSON bytes to Go value), struct tags for field control, and streaming codecs for large payloads. This guide covers each feature in depth with runnable examples.

Glossary

Marshaling
Converting a Go value (struct, map, slice, primitive) into its JSON byte representation. Called via json.Marshal(v) or json.NewEncoder(w).Encode(v).
Unmarshaling
Parsing JSON bytes and populating a Go value. Called via json.Unmarshal(data, &v) or json.NewDecoder(r).Decode(&v). Always pass a pointer so the value can be modified.
Struct tag
A raw string literal in backticks after a struct field declaration that provides metadata to reflection-based libraries. The json key controls encoding/json behavior: field name, omitempty, and string coercion.
RawMessage
A []byte type alias (type RawMessage []byte) in the encoding/json package. It passes raw JSON bytes through Marshal/Unmarshal untouched, enabling deferred or conditional decoding.
Zero value
Go's default value for each type: 0 for integers, 0.0 for floats, false for booleans, "" for strings, nil for pointers/slices/maps. The omitempty tag option omits a field if it holds its zero value.

Marshaling Structs to JSON

json.Marshal converts a Go value to a JSON-encoded []byte. Exported struct fields are included by default; their JSON keys default to the exact field name.

package main

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

type Address struct {
    Street string
    City   string
    Zip    string
}

type User struct {
    ID      int
    Name    string
    Email   string
    Address Address
}

func main() {
    u := User{
        ID:    42,
        Name:  "Ada Lovelace",
        Email: "ada@example.com",
        Address: Address{
            Street: "123 Main St",
            City:   "London",
            Zip:    "SW1A 1AA",
        },
    }

    data, err := json.Marshal(u)
    if err != nil {
        log.Fatalf("marshal error: %v", err)
    }
    fmt.Println(string(data))
    // {"ID":42,"Name":"Ada Lovelace","Email":"ada@example.com",
    //  "Address":{"Street":"123 Main St","City":"London","Zip":"SW1A 1AA"}}

    // Pretty-print with indentation
    pretty, _ := json.MarshalIndent(u, "", "  ")
    fmt.Println(string(pretty))
}

json.MarshalIndent(v, prefix, indent) adds whitespace for human-readable output. Use it for configuration files, debugging, or API responses where readability matters. For high-throughput APIs, prefer compact json.Marshal output.

Unmarshaling JSON to Structs

json.Unmarshal parses JSON bytes into a Go value. Always pass a pointer — otherwise the function cannot modify the original value.

package main

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

type Product struct {
    ID       int
    Name     string
    Price    float64
    InStock  bool
    Tags     []string
}

func main() {
    raw := []byte(`{
        "ID": 7,
        "Name": "Gopher Plushie",
        "Price": 19.99,
        "InStock": true,
        "Tags": ["toys", "go", "merch"]
    }`)

    var p Product
    if err := json.Unmarshal(raw, &p); err != nil {
        log.Fatalf("unmarshal error: %v", err)
    }

    fmt.Printf("Product: %+v
", p)
    // Product: {ID:7 Name:Gopher Plushie Price:19.99 InStock:true Tags:[toys go merch]}

    fmt.Printf("First tag: %s
", p.Tags[0])  // toys
}

Unknown JSON fields are silently ignored by default. Missing JSON fields leave the corresponding Go struct field at its zero value. To disallow unknown fields, use a json.Decoder with DisallowUnknownFields().

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

func strictUnmarshal(data []byte, v any) error {
    dec := json.NewDecoder(bytes.NewReader(data))
    dec.DisallowUnknownFields()
    return dec.Decode(v)
}

// Now an unknown field like "colour" will return an error:
// json: unknown field "colour"

Struct Tags: json:"name,omitempty"

Struct tags are the primary way to control how encoding/json handles each field: renaming keys, skipping zero values, and coercing types.

type Article struct {
    // Rename: JSON key is "id", not "ID"
    ID int `json:"id"`

    // Rename + omitempty: omit if Title == ""
    Title string `json:"title,omitempty"`

    // Completely skip this field in both marshal and unmarshal
    Internal string `json:"-"`

    // string option: encode int as a JSON string "42" instead of 42
    ViewCount int `json:"view_count,string"`

    // Pointer: distinguishes null from missing
    DeletedAt *string `json:"deleted_at,omitempty"`

    // Unexported — always ignored regardless of tags
    secret string
}

// Marshal example
a := Article{ID: 1, Title: "Go JSON", ViewCount: 100}
data, _ := json.Marshal(a)
// {"id":1,"title":"Go JSON","view_count":"100"}
// Note: Title present (non-zero), deleted_at absent (nil pointer + omitempty)

// Empty Title with omitempty
a2 := Article{ID: 2, ViewCount: 50}
data2, _ := json.Marshal(a2)
// {"id":2,"view_count":"50"}
// Title omitted because it's "" (zero value)

omitempty and Zero Values

The omitempty option omits a field if it equals its zero value. For pointers and slices, the zero value is nil. An empty slice ([]string{}) is not nil — it will still be included. Use a nil slice if you want omission; use an empty slice if you want [] in the output.

type Config struct {
    Timeout int      `json:"timeout,omitempty"`  // omitted if 0
    Debug   bool     `json:"debug,omitempty"`    // omitted if false
    Tags    []string `json:"tags,omitempty"`     // omitted if nil, NOT if []string{}
}

// nil slice → omitted
c1 := Config{Timeout: 30}
data1, _ := json.Marshal(c1)
// {"timeout":30}  — debug and tags omitted

// empty slice → NOT omitted
c2 := Config{Tags: []string{}}
data2, _ := json.Marshal(c2)
// {"tags":[]}  — timeout and debug omitted, tags included as []

Dynamic JSON with interface{} and map[string]interface{}

When you don't know the JSON structure at compile time, unmarshal into interface{} or map[string]interface{}. The JSON decoder maps JSON types to Go types automatically.

import (
    "encoding/json"
    "fmt"
)

func main() {
    raw := []byte(`{"name":"Gopher","age":5,"active":true,"scores":[10,20,30]}`)

    // Unmarshal into interface{}
    var v interface{}
    json.Unmarshal(raw, &v)

    // Type assert to map
    m := v.(map[string]interface{})
    fmt.Println(m["name"])    // Gopher (string)
    fmt.Println(m["age"])     // 5 (float64 — all JSON numbers become float64)
    fmt.Println(m["active"])  // true (bool)

    scores := m["scores"].([]interface{})
    for _, s := range scores {
        fmt.Printf("%.0f ", s.(float64))  // 10 20 30
    }

    // json.Number preserves numeric precision
    dec := json.NewDecoder(bytes.NewReader(raw))
    dec.UseNumber()
    var v2 interface{}
    dec.Decode(&v2)
    m2 := v2.(map[string]interface{})
    age := m2["age"].(json.Number)
    fmt.Println(age.String())  // "5" — no float64 precision loss
    n, _ := age.Int64()
    fmt.Println(n)             // 5
}

The key caveat: all JSON numbers become float64 when decoded into interface{}. For large integers (ID fields, timestamps), use dec.UseNumber() to preserve the original string representation via json.Number.

json.RawMessage for Deferred Decoding

json.RawMessage holds raw JSON bytes, passing them through marshaling and unmarshaling unchanged. This is the standard Go pattern for polymorphic payloads.

package main

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

// An event envelope where payload varies by type
type Event struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"`
}

type ClickEvent struct {
    X, Y int
    Target string
}

type FormEvent struct {
    FieldName string
    Value     string
}

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

    switch e.Type {
    case "click":
        var click ClickEvent
        json.Unmarshal(e.Payload, &click)
        fmt.Printf("Click at (%d,%d) on %s
", click.X, click.Y, click.Target)
    case "form":
        var form FormEvent
        json.Unmarshal(e.Payload, &form)
        fmt.Printf("Form: %s = %s
", form.FieldName, form.Value)
    default:
        fmt.Printf("Unknown event type: %s, raw payload: %s
", e.Type, e.Payload)
    }
}

func main() {
    processEvent([]byte(`{"type":"click","payload":{"X":100,"Y":200,"Target":"button#submit"}}`))
    processEvent([]byte(`{"type":"form","payload":{"FieldName":"email","Value":"user@example.com"}}`))
}

json.RawMessage also works in marshaling — embed it in a struct to inject pre-encoded JSON directly without re-encoding. This is efficient when you already have valid JSON bytes from a cache or upstream service.

// Embedding pre-encoded JSON into a response
cachedPayload := json.RawMessage(`{"items":[1,2,3],"total":3}`)

resp := struct {
    Status  string          `json:"status"`
    Data    json.RawMessage `json:"data"`
}{
    Status: "ok",
    Data:   cachedPayload,
}

out, _ := json.Marshal(resp)
fmt.Println(string(out))
// {"status":"ok","data":{"items":[1,2,3],"total":3}}

Streaming with json.Encoder and json.Decoder

json.NewEncoder(w).Encode(v) writes JSON directly to an io.Writer. json.NewDecoder(r).Decode(&v) reads from an io.Reader. Both stream data without requiring the full payload in memory.

package main

import (
    "encoding/json"
    "net/http"
)

type Response struct {
    Status string `json:"status"`
    Data   any    `json:"data"`
}

// HTTP handler using Encoder — writes directly to ResponseWriter
func handleGet(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    enc := json.NewEncoder(w)
    enc.SetIndent("", "  ")  // optional pretty-print
    if err := enc.Encode(Response{Status: "ok", Data: map[string]int{"count": 42}}); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

// HTTP handler using Decoder — reads from Request.Body
type CreateRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func handlePost(w http.ResponseWriter, r *http.Request) {
    var req CreateRequest
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    if err := dec.Decode(&req); err != nil {
        http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
        return
    }
    // use req.Name, req.Email ...
    json.NewEncoder(w).Encode(Response{Status: "created", Data: req})
}

Decoding a JSON Array Stream

For large JSON arrays (log files, data exports), decode element by element to keep memory usage flat:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "os"
)

type LogEntry struct {
    Level   string `json:"level"`
    Message string `json:"message"`
    Time    string `json:"time"`
}

func processLargeFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()

    dec := json.NewDecoder(f)

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

    count := 0
    for dec.More() {
        var entry LogEntry
        if err := dec.Decode(&entry); err != nil {
            return fmt.Errorf("decode entry %d: %w", count, err)
        }
        // Process entry — only one entry in memory at a time
        if entry.Level == "error" {
            fmt.Println("ERROR:", entry.Message)
        }
        count++
    }

    fmt.Printf("Processed %d entries
", count)
    return nil
}

Custom MarshalJSON and UnmarshalJSON

Implement the json.Marshaler and json.Unmarshaler interfaces to fully control how a type encodes and decodes. Common use cases: custom time formats, enums as strings, field transformations, and flattening nested structs.

package main

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

// Custom time type using a non-standard format
type CustomTime struct {
    time.Time
}

const customFormat = "02 Jan 2006"

func (ct CustomTime) MarshalJSON() ([]byte, error) {
    return json.Marshal(ct.Time.Format(customFormat))
}

func (ct *CustomTime) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    t, err := time.Parse(customFormat, s)
    if err != nil {
        return err
    }
    ct.Time = t
    return nil
}

type Event struct {
    Name      string     `json:"name"`
    CreatedAt CustomTime `json:"created_at"`
}

func main() {
    e := Event{
        Name:      "Launch",
        CreatedAt: CustomTime{time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)},
    }

    data, _ := json.Marshal(e)
    fmt.Println(string(data))
    // {"name":"Launch","created_at":"19 May 2026"}

    var e2 Event
    json.Unmarshal(data, &e2)
    fmt.Println(e2.CreatedAt.Format("2006-01-02"))  // 2026-05-19
}

Enum Marshaling

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", 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[name]
    if !ok {
        return fmt.Errorf("unknown status: %q", name)
    }
    *s = val
    return nil
}

// Marshal: StatusActive → "active"
// Unmarshal: "closed" → StatusClosed

Flattening Embedded Structs

// Using inline tag to flatten nested struct fields
type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
}

type Person struct {
    Name    string  `json:"name"`
    Address         `json:",inline"`  // not supported natively — use embedded struct
}

// Go encoding/json DOES support anonymous (embedded) struct flattening:
type PersonFlat struct {
    Name string `json:"name"`
    Address            // embedded — fields promoted to top level in JSON
}

p := PersonFlat{Name: "Ada", Address: Address{Street: "123 Main", City: "London"}}
data, _ := json.Marshal(p)
fmt.Println(string(data))
// {"name":"Ada","street":"123 Main","city":"London"}

FAQ

What does json.Marshal return in Go?

json.Marshal returns ([]byte, error). The []byte is the JSON-encoded output. The error is non-nil only for types that can't be marshaled: channels, functions, cyclic structures, and custom MarshalJSON implementations that return errors. Always check the error — even if you don't expect it, defensive error handling is idiomatic Go.

How do json struct tags work in Go?

Struct tags are backtick string literals after a field declaration. The json key controls encoding: the first value is the JSON key name, followed by optional comma-separated options. omitempty skips the field when it's a zero value. string encodes numbers and booleans as JSON strings. Use - to completely skip a field. Tags have no impact on unexported fields — those are always ignored.

Why are unexported struct fields ignored by encoding/json?

Go's reflection system enforces package-level access control. Fields beginning with a lowercase letter are unexported — they're not accessible via reflect.Value.Field from outside the defining package. encoding/json uses reflection to enumerate and access struct fields, so it simply cannot see unexported fields. This is intentional: it prevents packages from accidentally leaking internal state through JSON serialization.

What is json.RawMessage used for in Go?

json.RawMessage stores raw JSON bytes and passes them through marshaling/unmarshaling unchanged. It's the idiomatic solution for polymorphic payloads — unmarshal the envelope to read a discriminator field, then unmarshal the RawMessage into the appropriate concrete type. It also lets you inject pre-encoded JSON (from a cache, for example) into a struct without re-encoding.

When should I use json.Decoder instead of json.Unmarshal?

Use json.Decoder when the JSON comes from an io.Reader (HTTP request body, file, network connection) — it avoids reading the entire payload into a []byte first. Use it also when processing a stream of multiple JSON values (newline-delimited JSON), or when iterating over large arrays element by element with dec.More() and dec.Decode. For in-memory []byte, json.Unmarshal is simpler and slightly faster.

How do I implement custom JSON marshaling in Go?

Implement MarshalJSON() ([]byte, error) on your type for custom encoding, and UnmarshalJSON([]byte) error for custom decoding. These interfaces (json.Marshaler and json.Unmarshaler) are invoked automatically. For UnmarshalJSON, use a pointer receiver so the method can modify the receiver. A common pattern is to define a shadow struct or alias type inside the method to avoid infinite recursion when calling json.Marshal internally.

How do I marshal a Go map to JSON?

json.Marshal supports map[string]T for any marshallable T. Map keys must be strings, integers, or implement encoding.TextMarshaler. Map keys are sorted lexicographically in the output (deterministic ordering since Go 1.12). For unknown structures, unmarshal into map[string]interface{} — but remember all numbers become float64. Use dec.UseNumber() to get json.Number instead.

How do I handle null JSON values in Go structs?

Use pointer types (*string, *int, etc.) to represent nullable fields. A nil pointer marshals to JSON null; a non-nil pointer marshals to its pointed-to value. For unmarshaling, a JSON null sets the pointer to nil. Without a pointer, you cannot distinguish between "field was null" and "field was 0 / empty string" — both produce the same zero value. Combine pointer types with omitempty to omit nil fields entirely.

Further reading and primary sources