Skip to content

Day 036 — encoding/json: Marshal & Unmarshal

Month 2 · Week 2 · ⬅ Day 035 · Day 037 ➡ · Journal index

🎯 Learning Objective

Convert Go values to and from JSON with json.Marshal/Unmarshal, understand the reflection-based encoder's rules (exported fields, default type mappings), and stream JSON with Encoder/Decoder.

📚 Topics

  • json.Marshal / json.MarshalIndent / json.Unmarshal
  • Exported-fields-only rule · default Go↔JSON type mapping
  • Decoding into map[string]interface{} · the float64 number trap
  • json.NewEncoder/NewDecoder for streams

📖 Reading / Sources

📝 Notes

  • json.Marshal(v) walks v with reflection and returns []byte. Only exported (capitalized) struct fields are encoded; unexported fields are invisible to the encoder → [[json-exported-only]].
  • json.Unmarshal(data, &v) decodes into the value v points to — you must pass a pointer, or the decoded data has nowhere to go.
  • Default type mapping: JSON object→struct/map, array→slice, string→string, true/falsebool, number→float64 (into interface{}). The number→float64 trap loses int precision past 2^53; use a Decoder with UseNumber() or decode into a typed field when precision matters → [[json-number-trap]].
  • Unknown JSON keys are silently ignored; missing keys leave the Go zero value. Decoding is lenient by default — call dec.DisallowUnknownFields() to make extra keys an error.
  • MarshalIndent(v, "", " ") pretty-prints; good for fixtures/config, not hot paths.
  • Maps marshal with sorted keys (deterministic output); struct fields marshal in declaration order.
  • For streams (NDJSON, request bodies), prefer json.NewEncoder(w).Encode(v) / json.NewDecoder(r).Decode(&v) — they read/write incrementally and avoid buffering the whole payload. The Decoder returns io.EOF when the stream ends → [[json-streaming]].
  • A pointer field encodes the pointed-to value, or JSON null if nil — handy to distinguish "absent" from "zero".

💻 Code Examples

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Admin bool   `json:"-"` // never serialized
}

u := User{ID: 7, Name: "Ada", Admin: true}
b, _ := json.Marshal(u)         // {"id":7,"name":"Ada"}

var got User
_ = json.Unmarshal(b, &got)     // pointer destination, Admin stays false

Full code: examples/month-02/json-tags/main.go · Run: go run ./examples/month-02/json-tags

🏋️ Exercises / Practice

Exercise Status Link
Encode/Decode a sensor Reading round trip exercises/month-02/week-2/tempjson

🐛 Mistakes Made

  • Passed a struct value (not &v) to Unmarshaljson: Unmarshal(non-pointer ...).
  • Wondered why a lowercase name string field never appeared in the output — unexported fields are skipped by the encoder.
  • Decoded a big integer ID into interface{} and got a float64 with rounding — switched to a typed int64 field.

❓ Open Questions

  • When is json.RawMessage (defer decoding part of a payload) the right tool vs. a second Unmarshal pass?

🧠 Active Recall (answer without looking)

  1. Q: Why must the destination of json.Unmarshal be a pointer?

    A Unmarshal needs to *write* the decoded values into your variable. Without a pointer it would only receive a copy, so the result would be discarded. Passing a non-pointer is a runtime error.

  2. Q: You decode {"n": 42} into map[string]interface{}. What is the Go type of m["n"]?

    A `float64` — JSON has one numeric type, and the default mapping into `interface{}` is `float64`. Use a typed struct field or `Decoder.UseNumber()` to preserve integers exactly.

🪶 Feynman Reflection

JSON marshaling is a translator that walks your value with reflection and writes the equivalent text; unmarshaling reads that text back into a box you hand it by address. It only sees the public (exported) parts, fills in what it recognizes, and quietly skips the rest — lenient by default, strict only when you ask.

🕳️ Knowledge Gaps

  • json.RawMessage and partial/lazy decoding patterns.
  • Exactly when streaming with Decoder beats Unmarshal(ReadAll(...)).

✅ Summary

I can round-trip Go values and JSON, know the encoder only touches exported fields, remember the number→float64 trap, and can stream with Encoder/Decoder.

⏭️ Next Steps / Prep for Tomorrow

  • Day 037: shape the wire format precisely with struct tags and custom Marshaler/Unmarshaler.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): encoding/json marshal and unmarshal (day 036)