Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 29

    JSON Processing

    The encoding/json package marshals Go structs to JSON and unmarshals JSON into typed values.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    The encoding/json package marshals Go structs to JSON and unmarshals JSON into typed values. Struct tags control field names; omitempty skips zero values. For dynamic JSON, use json.RawMessage or map[string]any.

    Every REST API in Go uses encoding/json or frameworks built on it. Understanding Marshal/Unmarshal, Decoder/Encoder for streams, and custom MarshalJSON methods prepares you for production API development and third-party integration.

    Interviewers ask about json tags, unknown field handling, and time formatting — all covered here with Go 1.22+ patterns.

    The story

    A Cloudflare Workers KV sync service unmarshals edge configuration JSON into Go structs, validates fields, and re-marshals a normalized form for storage in etcd. Field tags like json:"origin_pool,omitempty" control serialization — mismatched tags cause silent data loss in production config pipelines.

    encoding/json is reflection-based and sufficient for most APIs; high-throughput services may use json/v2 or codegen alternatives, but the standard library patterns appear in every REST microservice.

    Understanding the topic

    Key concepts

    • json.Marshal struct → []byte; json.Unmarshal []byte → struct.
    • Struct tags: `json:"field_name,omitempty"`.
    • json.NewDecoder(r).Decode(&v) for streaming HTTP bodies.
    • json.RawMessage delays parsing of nested JSON.
    • Unmarshal unknown fields ignored by default.
    • time.Time marshals as RFC3339 string by default.

    Step-by-step explanation

    1. Marshal uses exported fields only; unexported fields skipped.
    2. Unmarshal requires pointer to target variable.
    3. Decoder.Decode reads one JSON value from stream.
    4. Encoder.Encode writes JSON + newline to writer.
    5. Custom types implement MarshalJSON/UnmarshalJSON.
    6. DisallowUnknownFields on Decoder rejects unexpected keys.

    Practical code example

    API DTO with tags, streaming decode, and custom error response:

    go
    package main
    import (
    "bytes"
    "encoding/json"
    "fmt"
    "time"
    )
    type CreateOrderRequest struct {
    ProductID string `json:"product_id"`
    Quantity int `json:"quantity"`
    Note *string `json:"note,omitempty"`
    }
    type OrderResponse struct {
    ID string `json:"id"`
    ProductID string `json:"product_id"`
    Quantity int `json:"quantity"`
    CreatedAt time.Time `json:"created_at"`
    }
    func decodeOrder(data []byte) (CreateOrderRequest, error) {
    var req CreateOrderRequest
    dec := json.NewDecoder(bytes.NewReader(data))
    dec.DisallowUnknownFields()
    if err := dec.Decode(&req); err != nil {
    return req, fmt.Errorf("decode order: %w", err)
    }
    return req, nil
    }
    func main() {
    payload := []byte(`{"product_id":"SKU-42","quantity":2}`)
    req, err := decodeOrder(payload)
    if err != nil {
    panic(err)
    }
    resp := OrderResponse{
    ID: "ord-001", ProductID: req.ProductID,
    Quantity: req.Quantity, CreatedAt: time.Now().UTC(),
    }
    out, _ := json.MarshalIndent(resp, "", " ")
    fmt.Println(string(out))
    }

    Output

    {
      "id": "ord-001",
      "product_id": "SKU-42",
      "quantity": 2,
      "created_at": "2026-07-01T12:00:00Z"
    }

    Line-by-line code explanation

    • json.Marshal(v) encodes a struct to []byte JSON.
    • json.Unmarshal(data, &v) decodes JSON into a pointer — required for population.
    • `json:"name,omitempty"` omits zero-value fields from output JSON.
    • `json:"-"` excludes a field from both encoding and decoding.
    • json.NewDecoder(r).Decode(&v) streams decode from an io.Reader without loading all bytes.
    • json.NewEncoder(w).Encode(v) streams encode to an io.Writer with optional indentation.
    • json.RawMessage delays parsing of nested JSON blobs — useful for polymorphic API payloads.
    • custom MarshalJSON() on a type overrides default encoding for special formats.

    Key takeaway: DisallowUnknownFields catches API contract drift. Pointer fields with omitempty omit nil from JSON. Use UTC for timestamps.

    Real-world use

    Where you'll use this in production

    • REST API request/response serialization.
    • Parsing webhook payloads from Stripe, GitHub, Slack.
    • Config files in JSON format for cloud services.
    • Structured logging output as JSON lines (JSONL).

    Best practices

    • Use struct tags matching API contract (snake_case common).
    • DisallowUnknownFields on external input during development.
    • Use json.Decoder for HTTP bodies — don't read all then unmarshal.
    • Store times as UTC; document timezone in API spec.
    • Use pointer fields for optional JSON properties.
    • Validate after unmarshal — json only checks syntax.

    Common mistakes

    • Unmarshal into non-pointer — silent no-op.
    • Exporting fields required for Marshal — lowercase fields ignored.
    • Float64 for money in JSON — use string or int64 cents.
    • Infinite recursion marshaling parent-child without json:"-" on backref.
    • Assuming Unmarshal validates business rules.

    Advanced interview questions

    Q1BeginnerMarshal vs Unmarshal?
    Marshal Go→JSON bytes; Unmarshal JSON bytes→Go struct.
    Q2Beginneromitempty purpose?
    Omit field from JSON when zero value — keeps payload small.
    Q3IntermediateHandle unknown JSON fields?
    Ignored by default; DisallowUnknownFields on Decoder to reject.
    Q4IntermediateCustom JSON for time format?
    Implement MarshalJSON on wrapper type or use json tag with custom type.
    Q5AdvancedParse polymorphic webhook events safely.
    Require type discriminator; switch on type; unmarshal into specific struct; reject unknown types.

    Summary

    encoding/json maps structs to JSON via tags and exported fields. Decoder/Encoder stream JSON for HTTP without buffering entire body. Validate after unmarshal; use DisallowUnknownFields for strict APIs. Pointer + omitempty for optional fields; UTC for timestamps. Next lesson: HTTP client with net/http.

    Ready to mark this lesson complete?Track your journey across the entire course.