JSON

By Syncfusion®, Inc. · Last updated

JSON in Go with encoding/json: JSON structure, json.Marshal and MarshalIndent, json.Unmarshal, struct tags, nested JSON, and uses like config files and API responses.

What Is JSON?

JSON (JavaScript Object Notation) = a simple text format for data, made of names and values.

{"name": "Asha", "class": "8A", "marks": 91}
  • People can read it.
  • Almost every language and website can read and write it.
  • Go handles it with the encoding/json package.

Simple lesson: JSON is the common text language that programs use to share data.

A Real-World Example

Your school sends report cards to parents' phone apps.

  • The school's server is written in Go.
  • The phone app is written in another language.
  • They cannot share Go structs directly.
  • So the server turns each report card into JSON text, and the app reads that text.

Now the mapping:

  • Struct → JSON text = encoding (also called marshaling).
  • JSON text → struct = decoding (also called unmarshaling).

Go reads struct tags to name JSON keys. You met tags in Structs and Methods.

Only exported fields (capital first letter) are written to JSON or read from it. See Encapsulation.

What You Will Learn

In this lesson, you will learn:

  1. What JSON is: its structure, objects and arrays.
  2. How to encode Go values with json.Marshal() and json.MarshalIndent().
  3. How to decode JSON with json.Unmarshal().
  4. How to go from struct to JSON and back, use tags and handle nested JSON.
  5. Common uses: config files, API responses, local storage and data exchange.

JSON Fundamentals

JSON Structure

JSON has six kinds of values:

JSONExampleGo type
string"Asha"string
number91, 3.5int, float64
booleantruebool
nullnullnil
object{"a": 1}struct or map[string]T
array[1, 2, 3]slice

JSON Objects

JSON object = name-value pairs inside { }.

  • Names are always in double quotes.
  • A colon : separates name and value.
  • Commas separate the pairs. No comma after the last one.

JSON Arrays

JSON array = a list of values inside [ ].

{
  "class": "8A",
  "students": ["Asha", "Meena"],
  "teacher": {"name": "Mrs. Iyer", "subject": "Maths"}
}
  • Values can be nested: an object inside an object, an array inside an object.

Simple lesson: JSON is built from objects { }, arrays [ ], strings, numbers, booleans and null.

Encoding JSON

Encode (marshal) = turn a Go value into JSON text.

json.Marshal()

Struct to JSON

package main

import (
	"encoding/json"
	"fmt"
)

type Student struct {
	Name  string
	Class string
	Marks int
}

func main() {
	s := Student{Name: "Asha", Class: "8A", Marks: 91}
	data, err := json.Marshal(s)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(data))
}
{"Name":"Asha","Class":"8A","Marks":91}
  • json.Marshal returns []byte and an error.
  • With no tags, the keys are the Go field names.

json.MarshalIndent()

MarshalIndent = the same, but spread over lines, easy to read.

Pretty JSON

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	timetable := map[string][]string{
		"Monday":  {"Maths", "English"},
		"Tuesday": {"Science"},
	}
	data, _ := json.MarshalIndent(timetable, "", "  ")
	fmt.Println(string(data))
}
{
  "Monday": [
    "Maths",
    "English"
  ],
  "Tuesday": [
    "Science"
  ]
}
  • The second argument is a prefix for each line (usually "").
  • The third is the indent: two spaces here.
  • Map keys come out sorted.

Simple lesson: json.Marshal turns Go values into JSON text, and MarshalIndent makes it pretty.

Decoding JSON

Decode (unmarshal) = read JSON text into a Go value.

json.Unmarshal()

Press Play to watch JSON fill a struct.

JSON text into a struct

Decode into a struct

package main

import (
	"encoding/json"
	"fmt"
)

type Book struct {
	Title string
	Pages int
}

func main() {
	text := []byte(`{"Title":"Malgudi Days","Pages":256}`)
	var b Book
	if err := json.Unmarshal(text, &b); err != nil {
		fmt.Println("bad JSON:", err)
		return
	}
	fmt.Println(b.Title, "has", b.Pages, "pages")
}
Malgudi Days has 256 pages
  • Pass a pointer: &b. See Pointers.
  • Unknown JSON keys are ignored.
  • Missing keys leave the field at its zero value.

Bad JSON

Broken JSON gives an error

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var m map[string]int
	err := json.Unmarshal([]byte(`{"maths": 91,}`), &m)
	fmt.Println(err != nil)
	fmt.Println(err)
}
true
invalid character '}' looking for beginning of object key string
  • The trailing comma is not allowed in JSON.
  • A wrong type, like text where a number is expected, also gives an error.

Decoding into a Map

When you do not know the keys in advance, decode into map[string]any.

Unknown shape: map[string]any

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	text := []byte(`{"name":"Meena","marks":85,"clubs":["art","chess"]}`)
	var data map[string]any
	json.Unmarshal(text, &data)
	fmt.Println(data["name"], data["marks"], data["clubs"])
	marks := data["marks"].(float64)
	fmt.Println(marks + 5)
}
Meena 85 [art chess]
90
  • JSON numbers become float64 in an any.
  • You need a type assertion to use them. See Interfaces and Polymorphism.
  • A struct is safer when you know the shape.

Simple lesson: json.Unmarshal(text, &value) fills your value, and returns an error for bad JSON.

Working with JSON

JSON Struct Tags

Tags control the JSON key names and options.

Flip the options and watch the JSON change.

TagEffect
`json:"name"`use the key name
`json:"remark,omitempty"`leave the field out when it is empty
`json:"-"`never write or read this field

Tags in action

package main

import (
	"encoding/json"
	"fmt"
)

type Student struct {
	Name     string   `json:"name"`
	Class    string   `json:"class"`
	Marks    int      `json:"marks"`
	Clubs    []string `json:"clubs,omitempty"`
	Password string   `json:"-"`
}

func main() {
	s := Student{Name: "Asha", Class: "8A", Marks: 91, Password: "secret"}
	data, _ := json.Marshal(s)
	fmt.Println(string(data))
}
{"name":"Asha","class":"8A","marks":91}
  • Clubs is empty, so omitempty left it out.
  • Password has json:"-", so it is never written.

Struct to JSON and JSON to Struct

A round trip

package main

import (
	"encoding/json"
	"fmt"
)

type Book struct {
	Title    string `json:"title"`
	Borrowed bool   `json:"borrowed"`
}

func main() {
	original := Book{"Gitanjali", true}
	data, _ := json.Marshal(original)
	fmt.Println(string(data))

	var copyBook Book
	json.Unmarshal(data, &copyBook)
	fmt.Println(copyBook == original)
}
{"title":"Gitanjali","borrowed":true}
true

Nested JSON

Nested structs make nested JSON.

A class with a teacher and students

package main

import (
	"encoding/json"
	"fmt"
)

type Teacher struct {
	Name    string `json:"name"`
	Subject string `json:"subject"`
}

type Class struct {
	Name     string   `json:"name"`
	Teacher  Teacher  `json:"teacher"`
	Students []string `json:"students"`
}

func main() {
	text := []byte(`{
		"name": "8A",
		"teacher": {"name": "Mrs. Iyer", "subject": "Maths"},
		"students": ["Asha", "Meena", "Kabir"]
	}`)
	var c Class
	if err := json.Unmarshal(text, &c); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(c.Teacher.Name, "teaches", c.Teacher.Subject)
	fmt.Println(len(c.Students), "students, first is", c.Students[0])
}
Mrs. Iyer teaches Maths
3 students, first is Asha
  • A JSON object inside an object maps to a struct field of struct type.
  • A JSON array maps to a slice.

Simple lesson: tags name the keys, and nested structs and slices match nested JSON.

Common Use Cases

Configuration Files

Read data/config.json

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

type Config struct {
	School string `json:"school"`
	Year   int    `json:"year"`
	Debug  bool   `json:"debug"`
}

func main() {
	data, err := os.ReadFile("data/config.json")
	if err != nil {
		fmt.Println(err)
		return
	}
	var cfg Config
	if err := json.Unmarshal(data, &cfg); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(cfg.School, cfg.Year, cfg.Debug)
}
Green Valley School 2025 false

Local Data Storage

Load, change and save a list

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

type Student struct {
	Name  string `json:"name"`
	Class string `json:"class"`
	Marks int    `json:"marks"`
}

func main() {
	data, _ := os.ReadFile("data/students.json")
	var list []Student
	json.Unmarshal(data, &list)

	list = append(list, Student{"Meena", "8A", 85})
	out, _ := json.MarshalIndent(list, "", "  ")
	os.WriteFile("data/students.json", out, 0644)

	saved, _ := os.ReadFile("data/students.json")
	var check []Student
	json.Unmarshal(saved, &check)
	fmt.Println(len(check), check[2].Name)
}
3 Meena
  • Read the file, decode, change the data, encode, and write it back.
  • This is how small apps save their data without a database. See File Handling.

REST API Responses and Data Exchange

API = a way for programs to ask each other for data, often over the web.

REST API response = the JSON a web server sends back.

// A tiny API on your computer: open http://localhost:8080/student
http.HandleFunc("/student", func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(Student{"Asha", "8A", 91})
})
  • json.NewEncoder(w).Encode(v) writes JSON straight to the web response.
  • json.NewDecoder(r.Body).Decode(&v) reads JSON straight from a request.
  • Web servers cannot run in this browser lab, so this is shown as a plain block.

JSON numbers decoded into any are always float64, even 91.

Decode into a struct with an int field when you need whole numbers.

Easy way to remember

  • Go → JSON: json.Marshal(v) or json.MarshalIndent(v, "", " ")
  • JSON → Go: json.Unmarshal(data, &v) (always &)
  • Key names: `json:"name"`. Skip empty: omitempty. Never: -.

Simple lesson: JSON is used for settings, saved data and talking to other programs.

TL;DR

  1. JSON = text data: objects { }, arrays [ ], strings, numbers, true/false, null.
  2. Encode = json.Marshal(v) → []byte. Pretty: json.MarshalIndent(v, "", " ").
  3. Decode = json.Unmarshal(data, &v). Pass a pointer. Check the error.
  4. Only exported fields take part.
  5. Tags = `json:"name"`, omitempty, "-".
  6. Nested JSON = nested structs and slices.
  7. Unknown shape = map[string]any. Numbers become float64.
  8. Uses = config files, API responses, local storage, data exchange.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Lowercase field namesAll fields are encodedOnly exported fieldsThe field is missing from JSON.
json.Unmarshal(data, s)A value worksPass &sjson: Unmarshal(non-pointer main.Student)
Ignoring the Unmarshal errorJSON is always validCheck errA half-empty struct used silently.
data["marks"].(int) from map[string]anyJSON numbers are intsThey are float64A type assertion panic.
Tag json:name without quotesQuotes are optionalWrite `json:"name"`The tag is ignored.
A trailing comma in JSONLike Go literalsJSON forbids itinvalid character error.
Printing data without string()It is textIt is []byteYou see a list of numbers.

Check Yourself

Exercises

What to Remember

  • JSON is text data that every language understands.
  • json.Marshal / MarshalIndent turn Go values into JSON.
  • json.Unmarshal(data, &v) fills your value. Always pass a pointer, always check the error.
  • Only exported fields are used. Tags set the names and options.
  • Nested structs and slices match nested JSON.

Next: Goroutines. You will learn how Go runs many jobs at the same time.