skip to content

What does json.NewDecoder(r.Body).Decode(&v) accept in a Go handler that a strict API should reject?

level: middleimportance: should knowfreq 48%

answer

  1. silence on a client's typo
  2. it stops after one value
  3. nothing bounds how much it reads
  4. decode again and demand io.EOF
  5. DisallowUnknownFields plus MaxBytesReader

basics

~20 s

By default it ignores JSON fields your struct does not declare, stops after the first JSON value so trailing data slips through, and reads without any size limit. Add DisallowUnknownFields, a second Decode expecting io.EOF, and http.MaxBytesReader.

solid answer

~40 s

A plain `json.NewDecoder(r.Body).Decode(&v)` is lenient in three ways. It silently drops keys that do not match a field, so a client typo like `{"emial": ...}` becomes a zero value instead of a 400 — `dec.DisallowUnknownFields()` turns that into an error. It reads exactly one JSON value and leaves the rest of the stream alone, so `{"a":1}{"b":2}` succeeds; calling `dec.Decode(&struct{}{})` again and requiring `io.EOF` rejects trailing data. And it will read as much as the client sends, so wrap the body in `http.MaxBytesReader(w, r.Body, n)` first and answer 413 when `errors.As` finds an `*http.MaxBytesError`. Finally, distinguish the error kinds: `io.EOF` means an empty body, `*json.SyntaxError` means malformed JSON, and `*json.UnmarshalTypeError` names the field whose type was wrong.

code

go · 13 lines
go
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
	r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	if err := dec.Decode(dst); err != nil {
		return err // io.EOF, *json.SyntaxError, *json.UnmarshalTypeError, ...
	}
	// a second Decode must report end of input, or the client sent extra data
	if err := dec.Decode(&struct{}{}); err != io.EOF {
		return errors.New("body must contain a single JSON value")
	}
	return nil
}

go deeper

for a junior

Know the basic call shape: construct a decoder over the request body, decode into a pointer, and check the error. Remember that fields your struct does not declare are simply ignored by default.

for a middle

Explain each default and its fix: DisallowUnknownFields for stray keys, a second Decode returning io.EOF for trailing data, and http.MaxBytesReader for an unbounded read.

for a senior

Show how you turn decode errors into responses a client can act on, distinguishing io.EOF, syntax errors and type errors, and how you factor that into one shared helper so strictness does not drift between endpoints.

for a principal

Own the tradeoff of strictness as a contract: rejecting unknown fields buys early detection internally but removes your freedom to drop a field later, so decide where that line sits for public versus internal surfaces and make it the default in shared code.

## The default is deliberately forgiving `json.NewDecoder(r.Body).Decode(&v)` streams one JSON value out of the request body and into `v`. Its defaults suit a tolerant reader, which is the right posture for consuming somebody else's API and the wrong one for validating input you are about to act on. Three gaps matter for a request handler. ### 1. Unknown fields are ignored Any key in the JSON object that does not match an exported field (by name or `json` tag) is skipped. A client that sends `{"emial": "[email protected]"}` gets a `200` and a user with an empty email, and a client still sending a field you removed last quarter gets no signal. `dec.DisallowUnknownFields()` — called on the `*json.Decoder` before `Decode` — makes that an error instead. Enable it on inbound request bodies where you own both sides, and think twice on a public API: strictness is a compatibility promise, since any field you later stop understanding becomes a hard failure for existing clients. ### 2. Only the first JSON value is consumed `Decode` reads one value and stops. A body of `{"a":1}{"b":2}` or `{"a":1} garbage` decodes the first object successfully and never looks at the rest. The standard fix is to decode again into a throwaway and insist on end-of-input: ```go if err := dec.Decode(&struct{}{}); err != io.EOF { return errors.New("body must contain a single JSON object") } ``` If that second call returns `nil`, the client sent a second value; if it returns any other error, the trailing bytes are not even valid JSON. Either way it is a 400. ### 3. The read is unbounded A `Decoder` will keep pulling bytes for as long as the client keeps sending them. Wrap the body before decoding: ```go r.Body = http.MaxBytesReader(w, r.Body, 1<<20) ``` `MaxBytesReader` returns an `io.ReadCloser` that fails once more than `n` bytes have been read, and it also tells the server not to keep the connection alive after the abort. Use `errors.As` against `*http.MaxBytesError` in your error handling and answer `http.StatusRequestEntityTooLarge`. ## Turning decode errors into good responses `Decode` returns several distinguishable errors, and mapping them is what separates a usable API from one that says "bad request": - `io.EOF` — the body was empty. Say so; clients hit this when a proxy strips the body or a `Content-Length: 0` sneaks through. - `io.ErrUnexpectedEOF` — the body was cut short mid-value. - `*json.SyntaxError` — malformed JSON; the error carries an `Offset` you can quote back. - `*json.UnmarshalTypeError` — well-formed JSON of the wrong shape, e.g. a string where an `int` was declared. It carries `Field` and `Value`, so you can say precisely which key was wrong. - The unknown-field error from `DisallowUnknownFields` is a plain error whose message names the offending key. Use `errors.As` for the typed ones and `errors.Is(err, io.EOF)` for the sentinel, then compose one response body for the client. ## Decoder versus Unmarshal `json.Unmarshal` takes a `[]byte`, so using it means reading the whole body into memory first with `io.ReadAll` — you own the buffer, and you can log the raw bytes, but you also hold it all at once. `json.NewDecoder(r.Body)` streams and never materialises the whole document as a single slice, which is why it is the default choice in handlers. Neither is safe without a cap on how much the client may send. ## Shape of a reusable helper Most codebases end up with one `decodeJSON(w, r, dst) error` function that does all of this: cap the body, construct the decoder, disallow unknown fields, decode, check for trailing data, and translate the error. Writing it once is what keeps the strictness consistent across every endpoint, and it is a good thing to be able to sketch on a whiteboard. ## Interview-ready summary The three defaults to name are: unknown fields ignored, one value read with the remainder skipped, and no size limit. The three fixes are `DisallowUnknownFields`, a second `Decode` that must return `io.EOF`, and `http.MaxBytesReader`. Then map `io.EOF`, `*json.SyntaxError` and `*json.UnmarshalTypeError` onto clear client-facing messages.

  • How do you tell an empty request body apart from malformed JSON?
    `Decode` returns `io.EOF` when the body contained no JSON value at all, so `errors.Is(err, io.EOF)` identifies an empty body. Malformed input gives a `*json.SyntaxError` (with an `Offset`), and a truncated body gives `io.ErrUnexpectedEOF`. Reporting those three differently saves clients a debugging round trip.
  • Would you enable DisallowUnknownFields on a public API?
    Cautiously. It is excellent when you own both sides, because it catches typos and stale clients immediately. On a public API it is a compatibility promise in reverse: every field you later remove or rename becomes a hard 400 for callers who still send it, so many teams keep it on internal endpoints and log-but-accept unknown fields on public ones.
  • Why decode from r.Body with a Decoder rather than io.ReadAll plus json.Unmarshal?
    The `Decoder` streams, so the whole document is never held as one slice, and it gives you the trailing-data check for free via a second `Decode`. `io.ReadAll` plus `Unmarshal` buffers everything first, which is only worth it when you also need the raw bytes — for logging or a signature check. Either way the body still needs a size cap.

saying these in an interview costs you the question

  • Assumes unknown JSON fields cause an error by default
  • Believes Decode fails on data after the first JSON value
  • Decodes an unbounded request body with no size cap
  • Returns a single generic 400 for every decode error
  • Thinks DisallowUnknownFields also enforces required fields
  • Says json.Unmarshal is safer than a Decoder for large bodies