DEV Community

Cover image for Mastering JSON the Go Way
Hugo Oliveira
Hugo Oliveira

Posted on

Mastering JSON the Go Way

One of the things I love about Go is how practical it is when dealing with JSON.
No complicated setup. No magic. Just clear, predictable code.

The encoding/json package provides two main ways to work with JSON:

- In-memory (Marshal/Unmarshal)
- Stream-based (Encoder/Decoder)

Let’s break it down.

1. Marshal and Unmarshal — for small or simple data

These are the most common functions when your JSON fits comfortably in memory.

▶️ Go → JSON (Marshal)

You convert a Go struct, map, or slice into JSON.

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

user := User{"Hugo", "hugo@example.com"}
data, err := json.Marshal(user)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))
Enter fullscreen mode Exit fullscreen mode

Output:

{"name":"Hugo","email":"hugo@example.com"}
Enter fullscreen mode Exit fullscreen mode

Use Marshal when:

  • You already have a Go object in memory.
  • You want to send or save it as JSON (HTTP response, file, etc.).
  • The data is not too large (fits easily in memory).

◀️JSON → Go (Unmarshal)

You convert JSON bytes into a Go struct or map.

jsonData := []byte(`{"name":"Hugo","email":"hugo@example.com"}`)

var user User
err := json.Unmarshal(jsonData, &user)
if err != nil {
    log.Fatal(err)
}

fmt.Println(user.Name)  // Hugo
Enter fullscreen mode Exit fullscreen mode

Use Unmarshal when:

  • You receive JSON as a string or from a file and want to turn it into a Go type.
  • The JSON is fully available in memory.

Tip:
Always pass a pointer to the destination (&user), so Go can modify the variable.

2. Encoder and Decoder — for streams or large data

When working with large JSON files or network streams, loading everything into memory isn’t efficient.
That’s where Encoder and Decoder come in — they work with streams (io.Reader and io.Writer).

▶️Go → JSON stream (Encoder)
You write JSON directly to an output, like a file or HTTP response.

user := User{"Hugo", "hugo@example.com"}
file, _ := os.Create("user.json")
defer file.Close()

encoder := json.NewEncoder(file)
encoder.SetIndent("", "  ") // optional, for pretty-print
encoder.Encode(user)
Enter fullscreen mode Exit fullscreen mode

Use Encoder when:

  • You want to write JSON directly to a file, socket, or HTTP response.
  • You want to avoid keeping the full JSON in memory.
  • You’re writing continuous data (e.g., logs, API responses).

◀️ JSON stream → Go (Decoder)

You read JSON progressively from an input, like a file or HTTP request body.

file, _ := os.Open("user.json")
defer file.Close()

var user User
decoder := json.NewDecoder(file)
decoder.Decode(&user)

fmt.Println(user)
Enter fullscreen mode Exit fullscreen mode

Use Decoder when:

  • You’re reading large JSON data.
  • The data is coming from a network or file stream.
  • You don’t want to load everything at once into memory.

Summary

Direction Data Type Function Best for
JSON → Go []byte Unmarshal Small data in memory
Go → JSON []byte Marshal Small data in memory
JSON → Go io.Reader Decoder Large files or streams
Go → JSON io.Writer Encoder Large files or streams

Go’s encoding/json package is a perfect example of the language’s philosophy:
simple, explicit, and reliable.

When you need something quick and small, use Marshal and Unmarshal.
When you’re dealing with streams, files, or large payloads, use Encoder and Decoder.

You get both simplicity and performance, and you always know exactly what your code is doing.

That’s Go.

Top comments (0)