Question

json.Unmarshal vs json.NewDecoder.Decode in Go

gojson

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.Decoder instead of json.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.Unmarshal takes a []byte
  • json.Decoder.Decode reads JSON from an io.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.Body from net/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, use json.Unmarshal
  • If you have an io.Reader, use json.Decoder

Encoding side

There is a matching idea for writing JSON:

  • json.Marshal returns []byte
  • json.NewEncoder(w).Encode(v) writes JSON directly to an io.Writer

So the choice is often:

SituationCommon tool
You already have bytesjson.Unmarshal / json.Marshal
You have a streamjson.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 → use json.Unmarshal
  • A package arriving through a tube → io.Reader stream → use json.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.

Take Quiz

Syntax and Examples

Core syntax

Decode from bytes with json.Unmarshal

var u User
err := json.Unmarshal(data, &u)
  • data must 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.Body is an io.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

  1. strings.NewReader(...) creates a reader.

    • It behaves like a stream of JSON text.
  2. json.NewDecoder(body) creates a JSON decoder.

    • The decoder is connected to that reader.
  3. Decode(&item) starts reading bytes from the reader.

    • It parses the JSON object.
    • It matches JSON fields to struct tags.
  4. The JSON field "name" is stored in item.Name.

  5. The JSON field "price" is stored in item.Price.

  6. If the JSON is invalid, Decode returns 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:

  1. The full JSON already exists in data.
  2. json.Unmarshal parses the bytes.
  3. It fills the struct fields.
  4. 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)
Take Quiz

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.

  • Unmarshal is correct when you already have []byte
  • Decoder is correct when you have an io.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

Featurejson.Unmarshaljson.NewDecoder(...).Decode(...)
Input type[]byteio.Reader
Best forData already in memoryStreams like HTTP bodies or files
Extra buffering step needed for resp.BodyYesNo
Memory usage for large inputUsually higherUsually lower
Simplicity with HTTP bodiesLess directMore direct
Good for raw-body logging firstYesLess convenient
Supports decoder options like DisallowUnknownFieldsNoYes

Related encoding comparison

TaskBytes approachStream approach
Encode JSONjson.Marshal(v)json.NewEncoder(w).Encode(v)
Decode JSONjson.Unmarshal(data, &v)json.NewDecoder(r).Decode(&v)

Simple rule of thumb

  • Use Unmarshal for bytes
  • Use Decoder for readers

That is usually a better guideline than saying one is always better.

Take Quiz

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.ReadAll over ioutil.ReadAll
  • Use Decoder when working directly with io.Reader
  • Use Unmarshal when 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.

Take Quiz

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." ]

Take Quiz

Keep learning

Related questions

View more