Question
I am building a Go API client that needs to send JSON in requests and decode JSON from HTTP responses.
From examples in libraries and codebases, I have seen two common ways to decode JSON:
body, err := ioutil.ReadAll(resp.Body)
if err == nil && body != nil {
err = json.Unmarshal(body, value)
}
or:
err = json.NewDecoder(resp.Body).Decode(value)
When working with an HTTP response body, which implements io.Reader, the second approach seems shorter and more direct. However, since both styles appear in real code, I want to understand whether one should be preferred over the other.
I also saw advice saying:
Please use
json.Decoderinstead ofjson.Unmarshal.
But the reason was not explained clearly. Should json.Unmarshal generally be avoided, or are there cases where it is still the right choice?
Short Answer
By the end of this page, you will understand the difference between json.Unmarshal and json.NewDecoder(...).Decode(...) in Go, especially when reading HTTP responses. You will learn how they differ in input type, memory usage, streaming behavior, and common real-world usage, so you can choose the right one for each situation.
Concept
json.Unmarshal and json.Decoder.Decode both convert JSON into Go values, but they work with different kinds of input.
json.Unmarshaltakes a[]bytejson.Decoder.Decodereads JSON from anio.Reader
That difference matters because HTTP response bodies are streams. A response body is not already a byte slice; it is read gradually from resp.Body.
json.Unmarshal
Use json.Unmarshal when you already have the full JSON in memory.
var user User
err := json.Unmarshal(data, &user)
This is common when:
- JSON comes from a byte slice
- you read a file fully first
- you want to log or inspect the raw JSON before decoding
- you need to decode the same raw bytes more than once
json.Decoder.Decode
Use json.NewDecoder(r).Decode(...) when JSON comes from a stream such as:
resp.Bodyfrom
Mental Model
Think of JSON data in two forms:
- A full package already on your desk →
[]byte→ usejson.Unmarshal - A package arriving through a tube →
io.Readerstream → usejson.Decoder
json.Unmarshal says: “Give me the whole JSON document now.”
json.Decoder says: “I will read the JSON as it arrives from this source.”
For an HTTP response body, you are usually dealing with the second case. The data is coming from a stream, so decoding directly from the stream is like opening the package as it comes in instead of first storing the entire package in another box.
Syntax and Examples
Core syntax
Decode from bytes with json.Unmarshal
var u User
err := json.Unmarshal(data, &u)
datamust be a[]byte- you must pass a pointer like
&u
Decode from a reader with json.NewDecoder(...).Decode(...)
var u User
err := json.NewDecoder(resp.Body).Decode(&u)
resp.Bodyis anio.Reader- the decoder reads from the stream directly
Example: decoding an HTTP response
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func fetchUser(resp *http.Response) (*User, error) {
defer resp.Body.Close()
user User
err := json.NewDecoder(resp.Body).Decode(&user); err != {
, err
}
&user,
}
Step by Step Execution
Consider this code:
type Item struct {
Name string `json:"name"`
Price int `json:"price"`
}
body := strings.NewReader(`{"name":"Book","price":25}`)
var item Item
err := json.NewDecoder(body).Decode(&item)
What happens step by step
-
strings.NewReader(...)creates a reader.- It behaves like a stream of JSON text.
-
json.NewDecoder(body)creates a JSON decoder.- The decoder is connected to that reader.
-
Decode(&item)starts reading bytes from the reader.- It parses the JSON object.
- It matches JSON fields to struct tags.
-
The JSON field
"name"is stored initem.Name. -
The JSON field
"price"is stored initem.Price. -
If the JSON is invalid,
Decodereturns an error.
Real World Use Cases
When to use json.NewDecoder(...).Decode(...)
HTTP API clients
A client receives JSON from resp.Body.
var result APIResponse
err := json.NewDecoder(resp.Body).Decode(&result)
This is the most common choice for API responses.
HTTP servers reading request bodies
Servers often decode incoming JSON requests directly from r.Body.
var input CreateUserRequest
err := json.NewDecoder(r.Body).Decode(&input)
Reading large JSON files
If a file is large, decoding from the file stream avoids an extra full-buffer copy.
file, _ := os.Open("data.json")
defer file.Close()
var cfg Config
err := json.NewDecoder(file).Decode(&cfg)
When to use json.Unmarshal
Cached or in-memory JSON
If JSON is already stored in Redis, memory, or a test fixture as bytes, Unmarshal is straightforward.
cfg Config
err := json.Unmarshal(cachedBytes, &cfg)
Real Codebase Usage
In real Go codebases, both approaches are used, but the context usually decides which one fits best.
Common patterns with Decoder
Decode HTTP request or response bodies directly
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("decode response: %w", err)
}
This is common because the body is already a reader.
Combine with guard clauses
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %s", resp.Status)
}
var result APIResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
This keeps error handling simple and linear.
Strict decoding with unknown-field checks
Developers sometimes make decoding stricter for APIs they control.
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&input); err != nil {
return err
}
This helps catch typos or unexpected client input.
Common patterns with
Common Mistakes
1. Thinking json.Unmarshal should always be avoided
This is not true.
Unmarshalis correct when you already have[]byteDecoderis correct when you have anio.Reader
2. Reading the whole body unnecessarily
Broken style for normal HTTP decoding:
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var result Result
err = json.Unmarshal(body, &result)
This is not wrong, but it may be unnecessary if all you want is structured decoding.
Better:
var result Result
err := json.NewDecoder(resp.Body).Decode(&result)
3. Forgetting to pass a pointer
Broken code:
var result Result
err := json.NewDecoder(resp.Body).Decode(result)
This will fail because decoding needs a pointer.
Correct:
result Result
err := json.NewDecoder(resp.Body).Decode(&result)
Comparisons
| Feature | json.Unmarshal | json.NewDecoder(...).Decode(...) |
|---|---|---|
| Input type | []byte | io.Reader |
| Best for | Data already in memory | Streams like HTTP bodies or files |
Extra buffering step needed for resp.Body | Yes | No |
| Memory usage for large input | Usually higher | Usually lower |
| Simplicity with HTTP bodies | Less direct | More direct |
| Good for raw-body logging first | Yes | Less convenient |
| Supports decoder options like |
Cheat Sheet
Quick reference
Decode from bytes
var v MyType
err := json.Unmarshal(data, &v)
Use when:
- you already have
[]byte - you need the raw JSON first
Decode from a stream
var v MyType
err := json.NewDecoder(r).Decode(&v)
Use when:
- data comes from
resp.Body - data comes from
r.Body - data comes from a file or network reader
Encode to bytes
data, err := json.Marshal(v)
Encode to a writer
err := json.NewEncoder(w).Encode(v)
Rules to remember
- Pass a pointer when decoding:
&v - Close HTTP response bodies:
defer resp.Body.Close() - Check HTTP status codes before decoding
- Prefer
io.ReadAlloverioutil.ReadAll
FAQ
Should I always use json.NewDecoder instead of json.Unmarshal in Go?
No. Use json.NewDecoder for io.Reader inputs like HTTP bodies and files. Use json.Unmarshal when you already have a []byte.
Is json.NewDecoder faster than json.Unmarshal?
Not in every situation. The main advantage is avoiding an extra full read into memory when your input is a stream.
Why is json.NewDecoder(resp.Body).Decode(&v) common in HTTP clients?
Because resp.Body is already an io.Reader, so decoding directly is simpler and usually more memory-efficient.
When is json.Unmarshal the better choice?
When the JSON is already loaded as bytes, such as test data, cached payloads, or data you need to inspect before decoding.
Can I log the raw response body if I use json.NewDecoder?
Not easily in the same direct way, because the decoder consumes the stream. If you need raw logging, read the body first and then use json.Unmarshal.
Mini Project
Description
Build a small Go function that fetches a JSON response from an HTTP endpoint and decodes it into a struct. This demonstrates the most common real-world case for json.NewDecoder: reading JSON directly from an HTTP response body without first loading the whole body into a byte slice.
Goal
Create a function that requests user data from an API and decodes the JSON response safely and clearly.
Requirements
[ "Send an HTTP GET request to a URL.", "Check that the response status code is 200 OK.", "Decode the JSON response body directly into a struct using json.NewDecoder.", "Close the response body properly.", "Return the decoded value and any errors." ]
Keep learning
Related questions
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Check if a Value Exists in a Slice in Go
Learn how to check whether a value exists in a slice in Go, and why Go has no Python-style `in` operator for arrays or slices.
Concatenating Slices in Go with append
Learn how to concatenate two slices in Go using append and the ... operator, with examples, pitfalls, and practical usage.