JSON Processing
The encoding/json package marshals Go structs to JSON and unmarshals JSON into typed values.
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
- Marshal uses exported fields only; unexported fields skipped.
- Unmarshal requires pointer to target variable.
- Decoder.Decode reads one JSON value from stream.
- Encoder.Encode writes JSON + newline to writer.
- Custom types implement MarshalJSON/UnmarshalJSON.
- DisallowUnknownFields on Decoder rejects unexpected keys.
Practical code example
API DTO with tags, streaming decode, and custom error response:
package mainimport ("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 CreateOrderRequestdec := 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[]byteJSON.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.RawMessagedelays 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?
Q2Beginneromitempty purpose?
Q3IntermediateHandle unknown JSON fields?
Q4IntermediateCustom JSON for time format?
Q5AdvancedParse polymorphic webhook events safely.
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.