JSON with encoding/json
JSON (JavaScript Object Notation) is the text format that web APIs, mobile apps and configuration files use to exchange data. A food delivery app sends your order to the server as JSON, and the server replies with JSON. Go's standard encoding/json package converts between JSON text and Go values, mostly by reading your struct definitions.
In this lesson you will turn structs into JSON (marshalling), read JSON into structs (unmarshalling), control field names with struct tags, stream JSON with encoders and decoders, handle JSON of unknown shape, and see what the new encoding/json/v2 package in Go 1.27 changes.
From struct to JSON
json.Marshal takes any value and returns the JSON as a []byte plus an error. Only exported fields (capital first letter) are included. A struct tag, the text in backticks after a field, sets the JSON name and options:
package main
import (
"encoding/json"
"fmt"
)
type Order struct {
ID int `json:"id"`
Customer string `json:"customer"`
Items []string `json:"items"`
Total int `json:"total_paise"`
Coupon string `json:"coupon,omitempty"`
}
func main() {
o := Order{ID: 501, Customer: "Meera", Items: []string{"dosa", "filter coffee"}, Total: 18500}
b, err := json.Marshal(o)
if err != nil {
fmt.Println("marshal failed:", err)
return
}
fmt.Println(string(b))
pretty, _ := json.MarshalIndent(o, "", " ")
fmt.Println(string(pretty))
}{"id":501,"customer":"Meera","items":["dosa","filter coffee"],"total_paise":18500}
{
"id": 501,
"customer": "Meera",
"items": [
"dosa",
"filter coffee"
],
"total_paise": 18500
}Marshal writes compact JSON with fields in the order they appear in the struct. MarshalIndent adds a prefix (here empty) and an indent (two spaces) for humans. The Coupon field is missing from both because of omitempty and its empty value. Without tags, the JSON keys would be the Go names, "ID", "Customer" and so on; most APIs use lower-case or snake_case names, so tags are normal.
Useful tag forms:
| Tag | Effect |
|---|---|
json:"name" | Use name as the JSON key |
json:"name,omitempty" | Leave out the field when it is false, 0, "", nil, or an empty slice or map |
json:"name,omitzero" | Leave out the field when it is its type's zero value (Go 1.24), including zero structs and times |
json:"-" | Never encode or decode this field |
From JSON to struct
json.Unmarshal reads JSON into a value. Pass a pointer so it can fill the value in. Fields missing from the JSON keep their zero values, and JSON keys with no matching field are ignored:
package main
import (
"encoding/json"
"fmt"
)
type Order struct {
ID int `json:"id"`
Customer string `json:"customer"`
Items []string `json:"items"`
Total int `json:"total_paise"`
}
func main() {
data := []byte(`{"id": 502, "customer": "Farhan", "items": ["biryani"], "total_paise": 32000, "rating": 5}`)
var o Order
if err := json.Unmarshal(data, &o); err != nil {
fmt.Println("bad JSON:", err)
return
}
fmt.Printf("%+v\n", o)
err := json.Unmarshal([]byte(`{"id": "five"}`), &o)
fmt.Println("error for wrong type:", err != nil)
}{ID:502 Customer:Farhan Items:[biryani] Total:32000}
error for wrong type: trueThe "rating" key had no field, so it was skipped. A JSON string where the struct expects an int is an error, and the message names the field and both types. JSON from users and other services can be anything, so always check the error. A raw string literal in backticks is handy for writing JSON in Go code, because it needs no escaping of double quotes.
omitempty, omitzero and nil slices
Small details in how values are encoded matter to the apps that read your JSON. Compare a nil slice with an empty one, and omitempty with omitzero:
package main
import (
"encoding/json"
"fmt"
"time"
)
type Profile struct {
Name string `json:"name"`
Phone string `json:"phone,omitempty"`
Tags []string `json:"tags"`
Joined time.Time `json:"joined,omitzero"`
Verified *bool `json:"verified,omitempty"`
}
func main() {
p := Profile{Name: "Kabir"}
b, _ := json.Marshal(p)
fmt.Println(string(b))
p.Tags = []string{}
p.Joined = time.Date(2026, time.March, 1, 10, 0, 0, 0, time.UTC)
p.Verified = new(true)
b, _ = json.Marshal(p)
fmt.Println(string(b))
}{"name":"Kabir","tags":null}
{"name":"Kabir","tags":[],"joined":"2026-03-01T10:00:00Z","verified":true}- A nil slice encodes as
null, but an empty slice encodes as[]. Many JavaScript front ends crash onnullwhere they expect a list, so initialise slices you send. omitemptynever treats a struct such astime.Timeas empty, which is whyomitzerowas added in Go 1.24: it drops the zero time.- A
*boolwithomitemptygives three states: missing (nil),trueorfalse.new(true)uses the Go 1.26new(expr)form from Pointers. time.Timeencodes as an RFC 3339 string, the standard date format of JSON APIs.
Streams: Encoder and Decoder
HTTP request bodies, files and network connections are io.Readers and io.Writers. Instead of reading everything into a []byte first, use json.NewDecoder(r).Decode(&v) and json.NewEncoder(w).Encode(v):
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
)
type Order struct {
Customer string `json:"customer"`
Items []string `json:"items"`
}
func main() {
body := strings.NewReader(`{"customer": "Isha", "items": ["idli"], "discount": 10}`)
dec := json.NewDecoder(body)
dec.DisallowUnknownFields()
var o Order
err := dec.Decode(&o)
fmt.Println("rejected unknown field:", err != nil)
enc := json.NewEncoder(os.Stdout)
enc.Encode(map[string]int{"rice": 50, "dal": 20})
}rejected unknown field: true
{"dal":20,"rice":50}DisallowUnknownFields turns unexpected keys into an error, which catches typos such as "custmer" in API requests. Encode writes the JSON followed by a newline, and like fmt, it writes map keys in sorted order, so maps give stable JSON. You will use this pair in every handler in net/http & a REST API.
JSON of unknown shape
When you cannot define a struct in advance, decode into map[string]any. JSON objects become maps, arrays become []any, and every number becomes a float64:
var data map[string]any
json.Unmarshal([]byte(`{"city": "Pune", "pin": 411001, "metro": true}`), &data)
fmt.Printf("%T %v\n", data["pin"], data["pin"]) // float64 411001You then need type assertions such as data["city"].(string) to use the values. Prefer structs whenever you know the shape: they are faster, checked by the compiler and self-documenting.
Common mistakes
- Lower-case field names.
name stringis unexported and silently skipped. UseName stringwith ajson:"name"tag. - Passing a value instead of a pointer to
Unmarshal.json.Unmarshal(data, o)returns an error; write&o. - Ignoring errors. Bad input is normal on the internet; check every
UnmarshalandDecode. - Expecting
intfrommap[string]any. JSON numbers decode asfloat64there.
Key points
json.Marshalandjson.Unmarshalconvert between Go values and JSON; only exported fields take part.- Struct tags set key names and options such as
omitempty,omitzeroand-. - Nil slices encode as
nulland empty slices as[];time.Timeencodes as RFC 3339. - Use
json.NewDecoder(r).Decodeandjson.NewEncoder(w).Encodewith readers and writers;DisallowUnknownFieldscatches unexpected keys. - Go 1.27 adds
encoding/json/v2with stricter defaults, and v1 now runs on the faster v2 engine.
Exercise
A school app in Lucknow receives this JSON for a student: {"name": "Ananya", "class": 10, "marks": {"maths": 91, "science": 87}}. Define a Student struct with fields Name, Class and Marks map[string]int, unmarshal the JSON, add an english mark of 78, and print the student back as indented JSON.
Show solution
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"name"`
Class int `json:"class"`
Marks map[string]int `json:"marks"`
}
func main() {
data := []byte(`{"name": "Ananya", "class": 10, "marks": {"maths": 91, "science": 87}}`)
var s Student
if err := json.Unmarshal(data, &s); err != nil {
fmt.Println("bad JSON:", err)
return
}
s.Marks["english"] = 78
out, err := json.MarshalIndent(s, "", " ")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(out))
}{
"name": "Ananya",
"class": 10,
"marks": {
"english": 78,
"maths": 91,
"science": 87
}
}Unmarshal creates the map for you, so adding a key is safe. Map keys come out sorted, which is why english appears first.
Quick Quiz
1. Which fields does json.Marshal include?
Unexported fields are invisible to encoding/json.
2. What does a nil []string field encode to with the classic encoding/json?
A nil slice encodes as null; an empty, non-nil slice encodes as [].
3. Why must you pass &o to json.Unmarshal?
Unmarshal writes into the value, which requires its address.
4. What does omitzero (Go 1.24) do that omitempty does not?
omitempty never omits structs; omitzero omits any zero value.
Learn Go with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
