Code of the Day
AdvancedProduction Go

JSON encoding

Marshal structs to JSON and decode JSON into structs — struct tags, omitempty, streaming with json.Decoder, and custom marshalling.

GoAdvanced11 min read
Recommended first
By the end of this lesson you will be able to:
  • Convert a Go struct to JSON with json.Marshal
  • Parse JSON into a struct with json.Unmarshal
  • Use struct field tags to control JSON key names and omit zero values
  • Decode a JSON stream with json.NewDecoder
  • Implement custom MarshalJSON and UnmarshalJSON methods

Nearly every web service exchanges data as JSON. Go's encoding/json package handles the most common cases with a small API surface: Marshal, Unmarshal, and their streaming counterparts. Learning the struct-tag system and a few edge cases covers the vast majority of production JSON work.

json.Marshal and json.Unmarshal

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Struct to JSON
u := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
data, err := json.Marshal(u)
// data: {"id":1,"name":"Alice","email":"alice@example.com"}

// JSON to struct
var decoded User
err = json.Unmarshal(data, &decoded)

returns []byte. json.Unmarshal fills the struct in-place; always pass a pointer.

Struct tags

A is a string in backticks after the field type:

type Product struct {
    ID          int     `json:"id"`
    Name        string  `json:"name"`
    Price       float64 `json:"price"`
    Description string  `json:"description,omitempty"`
    internal    string  // unexported — never serialised
}

Tag options:

  • json:"name" — use name as the JSON key instead of the field name.
  • json:"name,omitempty" — omit the field from output if it has its zero value (0, "", false, nil).
  • json:"-" — always omit this field from JSON, even if it is exported.

omitempty is particularly useful for optional fields in API responses. Without it, every response includes every field including empty ones, which inflates payload size and confuses clients that expect absent fields to mean "not set".

Streaming with json.NewDecoder

For HTTP request bodies and large files, decode directly from the reader rather than loading everything into memory first:

func handleCreate(w http.ResponseWriter, r *http.Request) {
    var req CreateRequest
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()   // reject unexpected keys
    if err := dec.Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // use req...
}

DisallowUnknownFields() makes the decoder return an error if the JSON contains keys not in the struct. This is useful for strict APIs that want to reject typos.

For responses, json.NewEncoder(w) encodes directly to the response writer without an intermediate buffer:

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(responseStruct)

Handling time.Time

time.Time marshals to RFC 3339 format by default:

type Event struct {
    CreatedAt time.Time `json:"created_at"`
}
// {"created_at":"2025-01-15T10:30:00Z"}

This is what most JSON APIs expect. If you need a different format, use a custom type.

Custom MarshalJSON / UnmarshalJSON

For types that need non-standard serialisation, implement json.Marshaler and json.Unmarshaler:

type Cents int

func (c Cents) MarshalJSON() ([]byte, error) {
    // Serialise as a decimal dollar amount
    return json.Marshal(fmt.Sprintf("%.2f", float64(c)/100))
}

func (c *Cents) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    var f float64
    if _, err := fmt.Sscan(s, &f); err != nil {
        return err
    }
    *c = Cents(f * 100)
    return nil
}

When implementing UnmarshalJSON, be careful not to call json.Unmarshal(data, c) recursively — you'll get infinite recursion. If you need to unmarshal into the same type, use a type alias to break the recursion: type raw Cents; json.Unmarshal(data, (*raw)(c)).

Working with unknown JSON structure

When the shape of the JSON is not known at compile time, use map[string]any or json.RawMessage:

var m map[string]any
json.Unmarshal(data, &m)
name := m["name"].(string)   // type assertion required

// json.RawMessage preserves raw JSON for later decoding
type Envelope struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"`
}

json.RawMessage is useful for event-based APIs where the payload shape depends on the type field.

Check your understanding

Knowledge check

  1. 1.
    A struct field is defined as Score int json:"score,omitempty". The Score is 0. What appears in the JSON output?
  2. 2.
    You can pass a struct value (not a pointer) to json.Unmarshal and it will populate the fields correctly.
  3. 3.
    When should you call dec.DisallowUnknownFields() on a json.Decoder?

Do it yourself

Define a Person struct with Name string, Age int, Email string, and Phone string. Use tags so email and phone are omitted from JSON when empty. Marshal a few instances and inspect the output.

go run main.go

Where to go next

Your services are running and talking JSON. The next lesson covers profiling — how to find and fix performance bottlenecks with go tool pprof and flame graphs.

Finished reading? Mark it complete to track your progress.

On this page