In this post, we will learn how to work with JSON in Go, in the simplest way possible.

We will learn how to convert from JSON raw data (strings or bytes) into Go types like structs, arrays, and slices, as well as unstructured data like maps and empty interfaces.

banner

JSON is used as the de-facto standard for data serialization, and by the end of this post, you’ll get familiar with how to marshal (encode) and unmarshal (decode) JSON in Go

Unmarshaling Raw JSON Data

The Unmarshal function provided by Go’s JSON standard library lets us parse raw JSON data in the form of []byte variables.

We can convert JSON strings into bytes and unmarshal the data into a variables address:

import "encoding/json"
//...

// ... 
myJsonString := `{"some":"json"}`

// `&myStoredVariable` is the address of the variable we want to store our
// parsed data in
json.Unmarshal([]byte(myJsonString), &myStoredVariable)
//...

Let’s look at the different variable types for myStoredVariable, and when you should use them.

There are two types of data you will encounter when working with JSON:

  1. Structured data
  2. Unstructured data

Structured Data (Decoding JSON Into Structs)

“Structured data” refers to data where you know the format beforehand. For example, let’s say you have a bird object, where each bird has a species field and a description field :

{
  "species": "pigeon",
  "description": "likes to perch on rocks"
}

To work with this kind of data, create a struct that mirrors the data you want to parse. In our case, we will create a bird struct which has a Species and Description attribute:

type Bird struct {
  Species string
  Description string
}

And unmarshal it as follows:

birdJson := `{"species": "pigeon","description": "likes to perch on rocks"}`
var bird Bird	
json.Unmarshal([]byte(birdJson), &bird)
fmt.Printf("Species: %s, Description: %s", bird.Species, bird.Description)
//Species: pigeon, Description: likes to perch on rocks

Try it here

By convention, Go uses the same title cased attribute names as are present in the case insensitive JSON properties. So the Species attribute in our Bird struct will map to the species, or Species or sPeCiEs JSON property.

JSON Arrays

Let’s look at how we can decode an array of objects, like below:

[
  {
    "species": "pigeon",
    "decription": "likes to perch on rocks"
  },
  {
    "species":"eagle",
    "description":"bird of prey"
  }
]

Since each element of the array has the structure of the Bird struct, you can unmarshal it by creating a slice of birds :

birdJson := `[{"species":"pigeon","decription":"likes to perch on rocks"},{"species":"eagle","description":"bird of prey"}]`
var birds []Bird
json.Unmarshal([]byte(birdJson), &birds)
fmt.Printf("Birds : %+v", birds)
//Birds : [{Species:pigeon Description:} {Species:eagle Description:bird of prey}]

Try it here

Nested Objects

Now, consider the case when you have a property called Dimensions, that measures the Height and Length of the bird in question:

{
  "species": "pigeon",
  "decription": "likes to perch on rocks"
  "dimensions": {
    "height": 24,
    "width": 10
  }
}

As with our previous examples, we need to mirror the structure of the object in our Go code. To add a nested dimensions object, lets create a dimensions struct :

type Dimensions struct {
  Height int
  Width int
}

Now, the Bird struct will include a Dimensions field:

type Bird struct {
  Species string
  Description string
  Dimensions Dimensions
}

We can unmarshal this data using the same method as before:

birdJson := `{"species":"pigeon","description":"likes to perch on rocks", "dimensions":{"height":24,"width":10}}`
var birds Bird
json.Unmarshal([]byte(birdJson), &birds)
fmt.Printf(bird)
// {pigeon likes to perch on rocks {24 10}}

Try it here

Primitive Types

We mostly deal with complex objects or arrays when working with JSON, but data like 3, 3.1412 and "birds" are also valid JSON strings.

We can unmarshal these values to their corresponding data type in Go by using primitive types:

numberJson := "3"
floatJson := "3.1412"
stringJson := `"bird"`

var n int
var pi float64
var str string

json.Unmarshal([]byte(numberJson), &n)
fmt.Println(n)
// 3

json.Unmarshal([]byte(floatJson), &pi)
fmt.Println(pi)
// 3.1412

json.Unmarshal([]byte(stringJson), &str)
fmt.Println(str)
// bird

Try it out

JSON Struct Tags - Custom Field Names

We saw earlier that Go uses convention to determine the attribute name for mapping JSON properties.

Although sometimes, we want a different attribute name than the one provided in your JSON data. For example, consider the below data:

{
  "birdType": "pigeon",
  "what it does": "likes to perch on rocks"
}

Here, we would prefer birdType to remain as the Species attribute in our Go code. It is also not possible for us to provide a suitable attribute name for a key like "what it does".

To solve this, we can use struct field tags:

type Bird struct {
  Species string `json:"birdType"`
  Description string `json:"what it does"`
}

Now, we can explicitly tell our code which JSON property to map to which attribute.

birdJson := `{"birdType": "pigeon","what it does": "likes to perch on rocks"}`
var bird Bird
json.Unmarshal([]byte(birdJson), &bird)
fmt.Println(bird)
// {pigeon likes to perch on rocks}

Try it here

Decoding JSON to Maps - Unstructured Data

If you don’t know the structure of your JSON properties beforehand, you cannot use structs to unmarshal your data.

Instead you can use maps. Consider some JSON of the form:

{
  "birds": {
    "pigeon":"likes to perch on rocks",
    "eagle":"bird of prey"
  },
  "animals": "none"
}

There is no struct we can build to represent the above data for all cases since the keys corresponding to the birds can change, which will change the structure.

To deal with this case we create a map of strings to empty interfaces:

birdJson := `{"birds":{"pigeon":"likes to perch on rocks","eagle":"bird of prey"},"animals":"none"}`
var result map[string]any
json.Unmarshal([]byte(birdJson), &result)

// The object stored in the "birds" key is also stored as 
// a map[string]any type, and its type is asserted from
// the `any` type
birds := result["birds"].(map[string]any)

for key, value := range birds {
  // Each value is an `any` type, that is type asserted as a string
  fmt.Println(key, value.(string))
}

Try it here

Each string corresponds to a JSON property, and its mapped any type corresponds to the value, which can be of any type. We then use type assertions to convert this any type into its actual type.

These maps can be iterated over, so an unknown number of keys can be handled by a simple for loop.

Validating JSON Data

In real-world applications, we may sometimes get invalid (or incomplete) JSON data. Let’s see an example where some of the data is cut off, and the resulting JSON string is invalid:

{
  "birds": {
    "pigeon":"likes to perch on rocks",
    "eagle":"bird of prey"

In actual applications, this may happen due to network errors or incomplete data written to files

If we try to unmarshal this, our code will panic:

birdJson := `{"birds":{"pigeon":"likes to perch on rocks","eagle":"bird of prey"`
var result map[string]any
json.Unmarshal([]byte(birdJson), &result)

Output:

panic: interface conversion: interface {} is nil, not map[string]interface {}

Try it here

We can, of course handle the panic and recover from our code, but this would not be idiomatic or readable.

Instead, we can use the json.Valid function to check the validity of our JSON data:

if !json.Valid([]byte(birdJson)) {
	// handle the error here
	fmt.Println("invalid JSON string:", birdJson)
	return
}

Now, our code will return early and give the output:

invalid JSON string: {"birds":{"pigeon":"likes to perch on rocks","eagle":"bird of prey"

Try it here

Marshaling JSON Data

The same rules that are used to decode a JSON string can be applied to encoding as well.

Marshaling Structured Data

Let’s consider our Go struct from before, and see the code required to get a JSON string from data of its type:

package main

import (
	"encoding/json"
	"fmt"
)

// The same json tags will be used to encode data into JSON
type Bird struct {
	Species     string `json:"birdType"`
	Description string `json:"what it does"`
}

func main() {
	pigeon := &Bird{
		Species:     "Pigeon",
		Description: "likes to eat seed",
	}

	// we can use the json.Marhal function to
	// encode the pigeon variable to a JSON string
	data, _ := json.Marshal(pigeon)
	// data is the JSON string represented as bytes
	// the second parameter here is the error, which we
	// are ignoring for now, but which you should ideally handle
	// in production grade code

	// to print the data, we can typecast it to a string
	fmt.Println(string(data))
}

This will give the output:

{"birdType":"Pigeon","what it does":"likes to eat seed"}

Try it out

Ignoring Empty Fields

In some cases, we would want to ignore a field in our JSON output, if its value is empty. We can use the “omitempty” property for this purpose.

For example, if the Description field is missing for the pigeon object, the key will not appear in the encoded JSON string incase we set this property:

package main

import (
	"encoding/json"
	"fmt"
)

type Bird struct {
	Species     string `json:"birdType"`
	// we can set the "omitempty" property as part of the JSON tag
	Description string `json:"what it does,omitempty"`
}

func main() {
	pigeon := &Bird{
		Species:     "Pigeon",
	}

	data, _ := json.Marshal(pigeon)

	fmt.Println(string(data))
}

This will give us the output:

{"birdType":"Pigeon"}

Try it out

If we want to always ignore a field, we can use the json:"-" struct tag to denote that we never want this field included:

package main

import (
	"encoding/json"
	"fmt"
)

type Bird struct {
	Species     string `json:"-"`
}

func main() {
	pigeon := &Bird{
		Species:     "Pigeon",
	}

	data, _ := json.Marshal(pigeon)

	fmt.Println(string(data))
}

This code will always print an empty JSON object:

{}

Try it out

Marshaling Slices

This isn’t much different from structs. We just need to pass the slice or array to the json.Marshal function, and it will encode data like you expect:

pigeon := &Bird{
  Species:     "Pigeon",
  Description: "likes to eat seed",
}

// Now we pass a slice of two pigeons
data, _ := json.Marshal([]*Bird{pigeon, pigeon})
fmt.Println(string(data))

This will give the output:

[{"birdType":"Pigeon","what it does":"likes to eat seed"},{"birdType":"Pigeon","what it does":"likes to eat seed"}]

Try it out

Marshaling Maps

We can use maps to encode unstructured data.

The keys of the map need to be strings, or a type that can convert to strings. The values can be any serializable type.

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	// The keys need to be strings, the values can be
	// any serializable value
	birdData := map[string]any{
		"birdSounds": map[string]string{
			"pigeon": "coo",
			"eagle":  "squak",
		},
		"total birds": 2,
	}

	// JSON encoding is done the same way as before	
	data, _ := json.Marshal(birdData)
	fmt.Println(string(data))
}

Output (Try it out):

{"birdSounds":{"eagle":"squak","pigeon":"coo"},"total birds":2}

Best Practices (Structs vs Maps)

As a general rule of thumb, if you can use structs to represent your JSON data, you should use them. The only good reason to use maps would be if it were not possible to use structs due to the uncertain nature of the keys or values in the data.

If we use maps, we will either need each of the keys to have the same data type, or use a generic type and convert it later.