When should you use json.NewDecoder(r).Decode(&v) instead of json.Unmarshal on a []byte?
answer
- one is fed bytes, one a reader
- who holds the whole document
- the stream form can be called again
- a clean end shows up as io.EOF
basics
~20 sUse json.NewDecoder when the JSON arrives as a stream, such as an HTTP body or a file, so it is decoded as bytes arrive. Use json.Unmarshal when you already hold the whole document in memory as a []byte.
solid answer
~40 sThey run the same decoder; they differ in where the bytes come from. `json.Unmarshal` takes a `[]byte` that must already be complete in memory, so decoding an HTTP body that way means an `io.ReadAll` first and one extra full copy. `json.NewDecoder(r)` wraps any `io.Reader` and decodes off the stream, which is why it is the idiomatic choice for `r.Body`, a file, or a network connection. A `json.Decoder` is also re-callable: each `Decode` reads the next value, and it returns `io.EOF` once the stream is exhausted, so a file of records written one JSON value per line is just a loop over `Decode`. Reach for `json.Unmarshal` when you already have the bytes and want to keep them, for example to log or re-parse them.
code
go · 11 lines// data is a []byte already in memory.
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return err
}
// r is an *http.Request: decode straight off the body.
var req CreateReport
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return err
}go deeper
Be ready to say which one takes an io.Reader and which takes a []byte, and to write the loop that decodes an HTTP request body into a struct and checks the error.
Explain the mechanics: the extra full copy an io.ReadAll introduces, why a decoder can be called repeatedly for successive values, and what io.EOF versus io.ErrUnexpectedEOF from Decode each mean.
Show the judgment about read-ahead and ownership: once a decoder wraps a reader it has consumed bytes past your value, so decide up front which component owns the stream, and know when keeping the raw bytes is worth more than the saved copy.
Frame it as a contract question. Whether your service accepts a single document or an open-ended stream shapes client behaviour, retry semantics and memory posture, and that is a decision to make once for the platform rather than per handler.
## The same decoder, two entry points `encoding/json` exposes two ways to turn JSON into Go values. `json.Unmarshal(data []byte, v any) error` takes a complete document that is already in memory. `json.NewDecoder(r io.Reader) *json.Decoder` wraps a reader, and `(*json.Decoder).Decode(v any) error` reads the next JSON value out of that stream. The mapping rules are identical: the same struct tags, the same exported-fields-only rule, the same handling of missing and extra fields. Choosing between them is a question about *where the bytes live*, never about how they are interpreted. ## Why the reader form is the default for network input An HTTP request body is an `io.ReadCloser`. To use `json.Unmarshal` on it you must first do `io.ReadAll(r.Body)`, which builds one contiguous `[]byte` holding the whole request, and then the decoder walks that slice to build your struct. The stream form skips the intermediate slice: the decoder reads into its own modest internal buffer and produces the Go value directly. For a report-export service that both accepts a filter document and re-reads its own outputs, that removed copy is real, measurable work: a benchmark run with `-benchmem` shows fewer bytes and fewer allocations per operation once the `io.ReadAll` disappears. Be precise about what this does *not* buy you. Streaming avoids buffering the raw text, but a single enormous JSON value still allocates a correspondingly enormous Go value as it decodes. Deciding how large an input you are willing to accept is a separate decision from choosing the decoder. ## Repeatability: the property that actually matters The deeper difference is that a `json.Decoder` has *position*. `json.Unmarshal` is a pure function over one document; call it twice on the same bytes and you get the same result twice. A decoder advances. Call `Decode` again and you get the *next* value in the stream. That is what makes it the tool for data with no known end: ``` {"id":1,"total":9} {"id":2,"total":4} {"id":3,"total":7} ``` That file is not a single JSON document, so `json.Unmarshal` on the whole thing fails; but a loop calling `Decode` on one decoder reads every record. Whitespace, including the newlines, is skipped between values, so the framing is a convenience for humans and for line-oriented tools rather than something the decoder requires. ## Knowing when the stream ended `Decode` reports the clean end of a stream by returning `io.EOF` — that is a normal terminating condition, not a failure, and the loop breaks on it. A value that starts and then runs out of bytes gives `io.ErrUnexpectedEOF` instead, and a malformed value gives a `*json.SyntaxError`. Those two are genuine errors. After a syntax error the decoder's position in the stream is no longer meaningful, so the right response is to abandon the stream rather than to keep looping. ## The read-ahead gotcha A `json.Decoder` reads from the underlying reader in chunks, so after `Decode` returns, the decoder has almost certainly consumed bytes *past* the end of the value it gave you. If you then hand the original `io.Reader` to another parser, those bytes are gone. `(*json.Decoder).Buffered()` returns an `io.Reader` over exactly that read-ahead, which is how you splice the two together if you truly need to switch parsers mid-stream. The simpler rule: once a decoder owns a reader, keep using that decoder. ## When `json.Unmarshal` is the better answer It is not merely a legacy API. If you already hold the bytes — a config file you read at startup, a payload you must also log, hash, or hand to a second decode pass — `json.Unmarshal` is the honest expression of that. Wrapping a `bytes.Reader` in a decoder just to avoid saying `Unmarshal` adds indirection and buys nothing. The one-line rule that survives interviews: reader in, use a `json.Decoder`; bytes in hand, use `json.Unmarshal`.
- Does Decode read only the bytes belonging to the value it returns?No. A `json.Decoder` reads from the underlying reader in chunks, so it usually consumes bytes past the end of the value. `(*json.Decoder).Buffered()` hands you an `io.Reader` over that read-ahead. In practice it means you cannot pass the original reader on to another parser once a decoder has touched it.
- What does Decode return at a clean end of stream versus a truncated value?A clean end gives `io.EOF`, which is the normal loop-terminating condition rather than a failure. A value that begins and then runs out of bytes gives `io.ErrUnexpectedEOF`, and malformed input gives a `*json.SyntaxError`. After a syntax error the decoder's stream position is no longer meaningful, so abandon the stream instead of continuing the loop.
- When is json.Unmarshal still the better choice for data that arrived over the network?When you need the raw bytes for something besides decoding — logging the exact payload, hashing it, verifying a signature, or decoding it a second time into a different shape. You already hold the buffer, so `json.Unmarshal` states that plainly; wrapping a `bytes.Reader` in a decoder to avoid it adds indirection and buys nothing.
saying these in an interview costs you the question
- Claims the two map JSON to Go types by different rules
- Says Decode caps how much memory a body can use
- Treats io.EOF from Decode as a failure to report
- Calls io.ReadAll then json.Unmarshal on every request body
- Believes Decode consumes exactly the bytes of one value