Day 089 — JSON Request/Response Helpers¶
Month 4 · Week 1 · ⬅ Day 088 · Day 090 ➡ · Journal index
🎯 Learning Objective¶
Write the writeJSON/decodeJSON helpers every Go service needs: correct
headers and status, strict and bounded decoding, and a uniform error envelope.
📚 Topics¶
json.Encoder/Decoder, struct tags,DisallowUnknownFieldshttp.MaxBytesReader,SetEscapeHTML(false), error mapping to status codes
📖 Reading / Sources¶
-
encoding/json· JSON and Go blog -
http.MaxBytesReader·MaxBytesError - Alex Edwards — How to parse JSON request bodies
📝 Notes¶
- writeJSON: set
Content-Type: application/json; charset=utf-8beforeWriteHeader(status), thenjson.NewEncoder(w).Encode(v). Headers freeze on first write, so order matters → [[http-handler]]. TheEncoderappends a trailing newline. - Call
enc.SetEscapeHTML(false)for APIs: the default escapes<,>,&to<etc., which is pointless noise outside HTML. - decodeJSON: bound the body with
http.MaxBytesReader(w, r.Body, 1<<20)so a giant payload can't exhaust memory (it triggers*http.MaxBytesError). Thendec.DisallowUnknownFields()so a client typo 400s instead of being silently dropped. - Reject multiple JSON values: after a successful
Decode, checkdec.More()— a well-formed request body holds exactly one value. - Map decode errors to status codes with
errors.As/errors.Is:*http.MaxBytesError→ 413,io.EOF(empty body) → 400,*json.SyntaxError/*json.UnmarshalTypeError→ 400. Validation failures (e.g. missing required field) → 422 Unprocessable Entity → [[error-wrapping]]. - Struct tags drive the wire shape:
json:"name",json:"qty,omitempty",json:"-"to skip. Unexported fields are never (de)serialised. - The server owns server-assigned fields (ids, timestamps): ignore any
client-supplied
idand set it yourself. - A generic
decodeJSON[T any]returns a typed value with no casting — clean and reusable → [[generics]].
💻 Code Examples¶
func writeJSON(w http.ResponseWriter, status int, v any) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status) // after headers, before body
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
return enc.Encode(v)
}
func decodeJSON[T any](w http.ResponseWriter, r *http.Request) (T, error) {
var v T
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // cap at 1 MiB
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // reject typo'd fields
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() { // body must hold a single JSON value
return v, errors.New("body must contain a single JSON object")
}
return v, nil
}
Full code:
examples/month-04/json-api/main.go· Run:go run ./examples/month-04/json-api
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
JSON/Error response helpers (httptest) |
✅ | exercises/month-04/week-1/respond |
🐛 Mistakes Made¶
- Called
w.WriteHeader(201)beforew.Header().Set("Content-Type", …)— the Content-Type never made it onto the response. Set headers first. - Used
json.Unmarshal(io.ReadAll(r.Body))with no size cap; switched to aDecoder+MaxBytesReaderto bound memory and get streaming decode.
❓ Open Questions¶
- Worth surfacing the exact offset from
*json.SyntaxErrorto clients, or is a generic "malformed JSON" safer? (Leaning generic to avoid leaking internals.)
🧠 Active Recall (answer without looking)¶
- Q: Why call
DisallowUnknownFieldson the decoder?
A
So a client field that doesn't match the struct (often a typo) returns a 400 instead of being silently ignored.- Q: What must you do before
WriteHeaderwhen sending JSON, and why?
A
Set the `Content-Type` header — headers freeze once the status line is written, so any header set after `WriteHeader` is lost.🪶 Feynman Reflection¶
Two small funnels handle all JSON I/O: one writes a value with the right header and status, the other reads one value strictly and safely. Bounding the body and disallowing unknown fields turns sloppy client input into clear 4xx errors instead of silent bugs.
🕳️ Knowledge Gaps¶
- Streaming very large JSON arrays with
Decoder.Token()instead of decoding the whole document at once.
✅ Summary¶
I have reusable, correct JSON helpers: proper headers/status, escape-free output, size-bounded strict decoding, and a clean mapping from decode errors to status codes.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 090: graceful shutdown & server timeouts.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦🟦⬜ |
Suggested commit: feat(examples): JSON request/response helpers (day 089)