EthicsComputerCourses
Join training +91 90532 10052

JSON with encoding/json

Go · Lesson 29 of 43 Advanced 6 min read Updated

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:

Go
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))
}
Output
{"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:

TagEffect
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:

Go
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)
}
Output
{ID:502 Customer:Farhan Items:[biryani] Total:32000}
error for wrong type: true

The "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:

Go
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))
}
Output
{"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 on null where they expect a list, so initialise slices you send.
  • omitempty never treats a struct such as time.Time as empty, which is why omitzero was added in Go 1.24: it drops the zero time.
  • A *bool with omitempty gives three states: missing (nil), true or false. new(true) uses the Go 1.26 new(expr) form from Pointers.
  • time.Time encodes 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):

Go
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})
}
Output
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:

Go
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 411001

You 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 string is unexported and silently skipped. Use Name string with a json:"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 Unmarshal and Decode.
  • Expecting int from map[string]any. JSON numbers decode as float64 there.

Key points

  • json.Marshal and json.Unmarshal convert between Go values and JSON; only exported fields take part.
  • Struct tags set key names and options such as omitempty, omitzero and -.
  • Nil slices encode as null and empty slices as []; time.Time encodes as RFC 3339.
  • Use json.NewDecoder(r).Decode and json.NewEncoder(w).Encode with readers and writers; DisallowUnknownFields catches unexpected keys.
  • Go 1.27 adds encoding/json/v2 with 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
Go
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))
}
Output
{
  "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?

2. What does a nil []string field encode to with the classic encoding/json?

3. Why must you pass &o to json.Unmarshal?

4. What does omitzero (Go 1.24) do that omitempty does not?

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