LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 3719 min read

JSON Encoding & Decoding

Learn how to encode Go structs to JSON and decode JSON back into structs, using struct tags, json.Marshal, and json.Unmarshal.

Introduction

JSON is the lingua franca of web APIs, and Go's standard library has first-class support for it through the encoding/json package. With just a struct and a couple of function calls, you can convert Go data to JSON for an API response, or parse incoming JSON straight into typed Go values.

What You Will Learn
  • How struct tags control JSON field names and behavior.
  • How to encode a Go struct into JSON with json.Marshal.
  • How to decode JSON into a Go struct with json.Unmarshal.
  • How to work with nested structs and slices in JSON.

Struct Tags

By default, encoding/json uses a struct field's exact name as the JSON key. Struct tags let you override that, rename fields, and control extra behavior like omitting empty values.

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Password string `json:"-"`
}
TagMeaning
`json:"id"`Use "id" as the JSON key instead of "ID".
`json:"email,omitempty"`Omit this field from the JSON entirely if it is the zero value.
`json:"-"`Never include this field in JSON output at all.

Encoding with json.Marshal

json.Marshal converts a Go value into JSON-encoded bytes. Only exported (capitalized) struct fields are considered — unexported fields are silently skipped.

package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
func main() {
u := User{ID: 1, Name: "Ava Lee", Email: "ava@example.com"}
data, err := json.Marshal(u)
if err != nil {
fmt.Println("marshal error:", err)
return
}
fmt.Println(string(data))
}
Output

Click Run to see what this code prints.

For readable, indented output (handy for logging or debugging), use json.MarshalIndent(u, "", " ") instead.

Decoding with json.Unmarshal

json.Unmarshal does the reverse: it parses JSON bytes into a Go value you provide a pointer to. The target struct's tags determine how JSON keys map back onto its fields.

package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
func main() {
raw := []byte(`{"id":2,"name":"Marco Diaz","email":"marco@example.com"}`)
var u User
if err := json.Unmarshal(raw, &u); err != nil {
fmt.Println("unmarshal error:", err)
return
}
fmt.Printf("%+v\n", u)
}
Output

Click Run to see what this code prints.

Notice the & before u — Unmarshal needs a pointer so it can write the decoded values directly into the struct you passed.

Working with Nested JSON

Structs can nest other structs and slices to mirror nested JSON objects and arrays, and Marshal/Unmarshal handle the nesting automatically.

package main
import (
"encoding/json"
"fmt"
)
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type Customer struct {
Name string `json:"name"`
Address Address `json:"address"`
Tags []string `json:"tags"`
}
func main() {
c := Customer{
Name: "Priya Nair",
Address: Address{
City: "Pune",
Country: "India",
},
Tags: []string{"vip", "newsletter"},
}
data, _ := json.MarshalIndent(c, "", " ")
fmt.Println(string(data))
}
Output

Click Run to see what this code prints.

Common Mistakes

Avoid These Mistakes
  • Passing a value instead of a pointer to json.Unmarshal — it will not compile or will silently do nothing.
  • Using unexported (lowercase) struct fields and wondering why they never appear in the JSON output.
  • Forgetting struct tags and getting Go's default capitalized field names in the output JSON.
  • Ignoring the error from Marshal/Unmarshal, masking malformed JSON or unsupported types.
  • Mismatching JSON types and Go types, such as decoding a JSON string into an int field.

Best Practices

  • Always add explicit json struct tags rather than relying on Go's default field-name casing.
  • Use omitempty for optional fields that should not clutter the output when empty.
  • Use json:"-" to keep sensitive fields like passwords out of JSON entirely.
  • Check errors from both Marshal and Unmarshal — malformed input is common at API boundaries.
  • For unknown or dynamic JSON shapes, decode into map[string]interface{} or use json.RawMessage.

Frequently Asked Questions

Most likely it is unexported (lowercase), or it has a json:"-" tag, or it is the zero value with omitempty set.

Yes, decode into a map[string]interface{} (or []interface{} for arrays) to work with arbitrary JSON, though you lose compile-time type safety.

json.Marshal returns a []byte in memory. json.NewEncoder writes JSON directly to an io.Writer, such as an HTTP response or a file, which avoids building the whole result in memory first.

Key Takeaways

  • Struct tags like `json:"name"` control how fields map to JSON keys.
  • json.Marshal converts a Go value into JSON bytes; only exported fields are included.
  • json.Unmarshal parses JSON into a Go value, which must be passed as a pointer.
  • Nested structs and slices map naturally to nested JSON objects and arrays.
  • omitempty and json:"-" give you fine control over what appears in the output.

Summary

encoding/json makes moving data between Go structs and JSON almost effortless, once you understand struct tags and the Marshal/Unmarshal pair. This is exactly the skill you need for the next lesson, where you will build a small web server that speaks JSON over HTTP.

Next Lesson →

Building a Web Server