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.Bodyfromnet/http- a file handle
- a network connection
- any
io.Reader
var user User
err := json.NewDecoder(resp.Body).Decode(&user)
This avoids reading the entire body into a separate byte slice first.
Why this matters
In real programs, especially API clients and servers, data often comes from streams. Using a decoder directly on the stream is usually:
- simpler
- more memory-efficient
- better suited for large payloads
- more natural for HTTP code
That does not mean json.Unmarshal is bad. It just means it solves a slightly different problem.
Important nuance
Advice like “always use json.Decoder instead of json.Unmarshal” is too broad.
A better rule is:
- If you already have
[]byte, usejson.Unmarshal - If you have an
io.Reader, usejson.Decoder
Encoding side
There is a matching idea for writing JSON:
json.Marshalreturns[]bytejson.NewEncoder(w).Encode(v)writes JSON directly to anio.Writer
So the choice is often:
| Situation | Common tool |
|---|---|
| You already have bytes | json.Unmarshal / json.Marshal |
| You have a stream | json.Decoder / json.Encoder |
This pattern appears throughout Go because many packages work with io.Reader and io.Writer.
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()
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, err
}
return &user, nil
}
This is the most common pattern for HTTP clients in Go.
Example: decode after reading all bytes
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func fetchUser(resp *http.Response) (*User, error) {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var user User
if err := json.Unmarshal(body, &user); err != nil {
return nil, err
}
return &user, nil
}
This also works, but it adds an extra step and stores the full body in memory.
When Unmarshal is a good fit
data := []byte(`{"id":1,"name":"Ada"}`)
var user User
err := json.Unmarshal(data, &user)
Here the JSON is already available as bytes, so Unmarshal is the natural choice.
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.
After decoding, item contains:
Item{Name: "Book", Price: 25}
Equivalent flow with Unmarshal
data := []byte(`{"name":"Book","price":25}`)
var item Item
err := json.Unmarshal(data, &item)
Step by step:
- The full JSON already exists in
data. json.Unmarshalparses the bytes.- It fills the struct fields.
- If parsing fails, it returns an error.
The decoding result is the same. The main difference is where the JSON came from and whether you already had all bytes in memory.
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.
var cfg Config
err := json.Unmarshal(cachedBytes, &cfg)
Tests
Tests often use raw literals for simple setup.
data := []byte(`{"enabled":true}`)
var c Config
_ = json.Unmarshal(data, &c)
Logging or debugging raw payloads
If you need to inspect the exact response body before decoding, you may read all bytes first.
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
log.Printf("raw response: %s", body)
err = json.Unmarshal(body, &result)
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 Unmarshal
Decode after preprocessing bytes
Sometimes code must decompress, decrypt, or transform raw bytes first.
raw := decrypt(payload)
var result Message
if err := json.Unmarshal(raw, &result); err != nil {
return err
}
Decode the same body in multiple ways
If code needs the raw bytes for audit logs and structured decoding, Unmarshal may be more convenient.
Test helpers and fixtures
Real codebases frequently use Unmarshal in tests because test data is often already available as a string or byte slice.
Good hygiene in HTTP code
No matter which method you choose:
- close the response body
- check status codes before trusting the body format
- wrap errors with context
- avoid decoding into the wrong type
- keep request and response structs explicit
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:
var result Result
err := json.NewDecoder(resp.Body).Decode(&result)
4. Forgetting to close the response body
Broken code:
resp, err := http.Get(url)
if err != nil {
return err
}
var result Result
return json.NewDecoder(resp.Body).Decode(&result)
Correct:
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
var result Result
return json.NewDecoder(resp.Body).Decode(&result)
5. Ignoring HTTP status codes
A non-200 response might return HTML or an error JSON structure instead of the expected data.
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %s", resp.Status)
}
6. Assuming Decoder always validates that nothing extra exists
Decode reads one JSON value. In some cases, extra data after that value may remain unread.
If you need stricter validation of a full body, you may need extra checks.
7. Using deprecated ioutil.ReadAll in newer Go code
Use io.ReadAll instead.
body, err := io.ReadAll(resp.Body)
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 DisallowUnknownFields | No | Yes |
Related encoding comparison
| Task | Bytes approach | Stream approach |
|---|---|---|
| Encode JSON | json.Marshal(v) | json.NewEncoder(w).Encode(v) |
| Decode JSON | json.Unmarshal(data, &v) | json.NewDecoder(r).Decode(&v) |
Simple rule of thumb
- Use
Unmarshalfor bytes - Use
Decoderfor readers
That is usually a better guideline than saying one is always better.
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 - Use
Decoderwhen working directly withio.Reader - Use
Unmarshalwhen JSON is already a byte slice
Helpful strict mode
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
err := dec.Decode(&v)
Common edge case
Decode reads one JSON value. If you need to ensure the body contains exactly one valid JSON value and nothing else, add stricter validation logic.
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.
Does json.Decoder provide features that json.Unmarshal does not?
Yes. For example, you can use decoder options like DisallowUnknownFields().
Should I use ioutil.ReadAll in modern Go?
No. Prefer io.ReadAll in newer Go code.
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
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.