Skip to content

Expert · Standard library essentials · topic 33 of 46 · 40 min

JSON with encoding/json

Explanation

json.Marshal turns Go values into JSON and json.Unmarshal (or a json.Decoder) turns JSON into Go values. Only exported (capitalised) struct fields are included. Struct tags like `json:"name,omitempty"` set the JSON key and can skip empty values. Unknown JSON keys are ignored unless you ask the decoder to reject them.

Why JSON?

JSON (JavaScript Object Notation) is the text format almost every web API uses: {"name":"Asha","marks":91}. Your Go service reads JSON from requests and writes JSON in responses. The encoding/json package converts between JSON text and Go structs, maps and slices.

Go value to JSON: Marshal

go
type Product struct {
	ID       int      `json:"id"`
	Name     string   `json:"name"`
	Price    int      `json:"price_paise"`
	Tags     []string `json:"tags,omitempty"`
	internal string   // unexported: never in JSON
}

p := Product{ID: 1, Name: "Masala chai", Price: 1500}
b, err := json.Marshal(p)
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(b))
// {"id":1,"name":"Masala chai","price_paise":1500}
Marshal returns []byte. Tags is nil, so omitempty leaves it out. Field order follows the struct.

Struct tags

TagEffect
`json:"name"`Use "name" as the JSON key
`json:"name,omitempty"`Skip the field when it is empty: 0, "", false, nil, or an empty slice or map
`json:"-"`Never include this field (for example a password hash)
`json:",string"`Encode a number or bool as a JSON string, like "1500"
`json:"name,omitzero"`Go 1.24+: skip the field when it is its zero value (works for structs and time.Time too)
Careful: The most common JSON bug in Go: lowercase field names. type User struct { name string } marshals to {} because unexported fields are invisible to encoding/json. Capitalise the field and use a tag for the lowercase key: Name string `json:"name"`.

JSON to Go value: Unmarshal

go
data := []byte(`{"id":7,"name":"Filter coffee","price_paise":2500,"extra":"ignored"}`)
var p Product
if err := json.Unmarshal(data, &p); err != nil {
	log.Fatal(err)
}
fmt.Printf("%+v\n", p)
// {ID:7 Name:Filter coffee Price:2500 Tags:[] internal:}
Pass a POINTER (&p) so Unmarshal can fill it. Unknown keys like "extra" are silently ignored. Keys are matched to fields case-insensitively.

If a JSON value has the wrong type, Unmarshal returns an error such as json: cannot unmarshal string into Go struct field Product.price_paise of type int. Missing keys simply leave the field at its zero value, so check required fields yourself.

Streams: Decoder and Encoder

go
// in an HTTP handler
func createProduct(w http.ResponseWriter, r *http.Request) {
	var in Product
	dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
	dec.DisallowUnknownFields()            // reject typos like "prise_paise"
	if err := dec.Decode(&in); err != nil {
		http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(in)         // writes JSON plus a newline
}
Decoder reads from any io.Reader (a request body, a file); Encoder writes to any io.Writer. MaxBytesReader stops huge bodies.

Maps, slices and unknown shapes

go
counts := map[string]int{"tea": 3, "coffee": 1}
b, _ := json.Marshal(counts)
fmt.Println(string(b)) // {"coffee":1,"tea":3}  (map keys are sorted)

var nilList []int
emptyList := []int{}
b1, _ := json.Marshal(nilList)
b2, _ := json.Marshal(emptyList)
fmt.Println(string(b1), string(b2)) // null []
Return an empty slice, not a nil one, when an API should send [] instead of null.

Decoding into map[string]any or any works for unknown shapes, but every number becomes float64 and you need type assertions (module 4). When you know the shape, a struct is clearer and safer.

Other handy facts

  • json.MarshalIndent(v, "", " ") gives pretty output for logs and files.
  • time.Time encodes as an RFC 3339 string, like "2026-08-15T10:30:00+05:30".
  • []byte encodes as a base64 string.
  • A type can control its own JSON by implementing MarshalJSON and UnmarshalJSON methods.
  • Use a pointer field (*int) when you must tell "missing" (nil) from 0.
Tip: Keep API structs (what goes over the wire) separate from database structs when they differ. Then you never leak a field like password_hash by accident; mark such fields json:"-" anyway.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

JSON with encoding/json · 1/7

encoding/json

Almost every API speaks JSON. encoding slash json converts between JSON and Go values.

Prefer a video by a teacher? Find free videos on “golang encoding json marshal unmarshal struct tags omitempty” on YouTube ↗

In real life: A UPI payment webhook

When a UPI payment succeeds, the payment gateway POSTs a JSON webhook to your server. Your handler decodes it into a struct with tags matching the gateway's snake_case keys, rejects bodies over 1 MB, ignores fields you do not use, and replies with a small JSON acknowledgement. Real gateways also send a signature header that you must verify before trusting the body.

go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

type PaymentEvent struct {
	OrderID string `json:"order_id"`
	Status  string `json:"status"`
	Paise   int64  `json:"amount_paise"`
	VPA     string `json:"payer_vpa,omitempty"`
}

func webhook(w http.ResponseWriter, r *http.Request) {
	var ev PaymentEvent
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&ev); err != nil {
		http.Error(w, "bad payload", http.StatusBadRequest)
		return
	}
	// TODO in real code: verify the gateway's signature header first
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"received": ev.OrderID})
}

func main() {
	body := `{"order_id":"ORD-77","status":"SUCCESS","amount_paise":49900,"bank_ref":"X1"}`
	rec := httptest.NewRecorder()
	webhook(rec, httptest.NewRequest(http.MethodPost, "/webhook", strings.NewReader(body)))
	fmt.Print(rec.Code, " ", rec.Body.String()) // 200 {"received":"ORD-77"}
}
httptest.NewRecorder lets you call a handler directly and inspect what it wrote. bank_ref is ignored because the struct has no such field.

Practice (0/4 done)

Task 1 · Question

What does this print? type U struct { Name string age int } b, _ := json.Marshal(U{"Asha", 21}) fmt.Println(string(b))

Task 2 · Question

What does this print? type P struct { Tags []string `json:"tags,omitempty"` Qty int `json:"qty"` } b, _ := json.Marshal(P{}) fmt.Println(string(b))

Task 3 · Question

Why does json.Unmarshal(data, p) fail to fill p when p is a Product value?

Task 4 · Practice

Define a struct Order with ID (json "id"), Items []string (json "items"), Total int64 (json "total_paise") and Note string (json "note,omitempty"). Marshal one order with MarshalIndent and print it. Then decode {"id":5,"items":["dosa"],"total_paise":4000,"coupon":"X"} with a Decoder that disallows unknown fields and print the error you get.

Quick quiz

Question 1

1.Which struct fields does encoding/json see?

Question 2

2.What does `json:"-"` do?

Question 3

3.What happens to a JSON key that has no matching struct field?

Question 4

4.In what order are map keys written by json.Marshal?

Question 5

5.Which writes JSON directly to an http.ResponseWriter?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which struct fields does encoding/json see?

Answer: Only exported (capitalised) fields

Unexported fields are ignored when encoding and decoding.

What does `json:"-"` do?

Answer: Excludes the field from JSON

A dash tag hides the field.

What happens to a JSON key that has no matching struct field?

Answer: It is ignored, unless the Decoder uses DisallowUnknownFields

By default unknown keys are skipped.

In what order are map keys written by json.Marshal?

Answer: Sorted

encoding/json sorts map keys, so output is stable.

Which writes JSON directly to an http.ResponseWriter?

Answer: json.NewEncoder(w).Encode(v)

An Encoder writes to any io.Writer.

How does Go reuse code from another struct type?Related

Answer: Embedding (composition)

Embedding promotes the inner type's fields and methods to the outer type.

From Structs and methods

A JSON field may be null or missing. Which field type fits?Related

Answer: Option<String>

Option maps null and missing keys to None.

From Serde and JSON

What is a struct?Related

Answer: A custom type that groups named fields

Structs group related values under one named type.

From Structs: your own data types

What happens when JSON.parse gets broken text?Related

Answer: It throws a SyntaxError

JSON.parse throws on bad input, so wrap it in try and catch.

From JSON: the language of APIs

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Standard library essentials” before its exam: Files, time and structured logs.
  2. Module 7 exam: Standard library essentials · 14 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Building a REST API with a database. 46 topics left in the course before the final exam.

← Previous: Calling other APIs safely with the HTTP client