json.Unmarshal vs json.Decoder: when to use each, and how do you safely decode a request body?
Unmarshal works on a complete []byte and fails if there is trailing garbage. Decoder reads from an io.Reader, buffers internally, and can decode a stream of values (NDJSON) or tokens — but Decode reads only the first value and silently ignores what follows ({"a":1}garbage succeeds).
A hardened request decoder:
func decodeJSON[T any](w http.ResponseWriter, r *http.Request) (T, error) {
var v T
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB cap
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
var mbe *http.MaxBytesError
if errors.As(err, &mbe) {
return v, fmt.Errorf("body too large")
}
return v, fmt.Errorf("bad json: %w", err)
}
// Anything after the first value (even another object) is an error.
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return v, errors.New("body must contain a single JSON object")
}
return v, nil
}
Key points: always cap body size (otherwise a client can stream gigabytes into memory), use DisallowUnknownFields for strict APIs to catch typos, and check for extra data by decoding a second time and expecting io.EOF (dec.More() is meant for array/object elements and misses trailing input such as a stray }). Distinguish *json.SyntaxError and *json.UnmarshalTypeError to return helpful 400 messages. For huge arrays, use dec.Token() plus dec.More() to stream element by element.
More on Standard Library, HTTP & Systems Design in Go
- Q452What does this print? How do you preserve large integer precision when decoding unknown JSON?
- Q453You implemented MarshalJSON but it isn't called. Why? Explain receiver rules and the recursion trap.
- Q455What does this print? (nil vs empty slices and maps in JSON)
- Q456How does database/sql connection pooling work? Which settings matter and what causes connection leaks?
- Q457How do you handle transactions, NULLs and "no rows" correctly with database/sql?
- Q458What is the exact io.Reader contract? What's wrong with this read loop?