12 minutes read

You already know that JSON (JavaScript Object Notation) is a text-based format for storing and transmitting structured data and that a basic JSON object could look like this:

{
    "code": "USD",
    "conversionRate": 1.1053,
    "inverseConvRate": 0.9047
}

Since we can build JSON text on one of two structures — a collection of key : value pairs or an orderly set of values — we can easily associate these two JSON structures to a map and a slice in Go.

In this topic, we'll learn how to encode and decode JSON maps and slices in Go, using functions within the encoding/json package.

Serializing a map

Encoding to JSON format is usually referred to as serialization. The encoding/json package in Go has two functions for serializing: json.Marshal() and json.MarshalIndent(). The key difference between these two functions is that json.MarshalIndent() allows us to indent and format the serialized output.

Let's take a look at how to use json.Marshal():

package main

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

func main() {
    actor := map[string]interface{}{"name": "Keanu Reeves", "age":  57}
    
    // The json.Marshal() function encodes/marshals the 'actor' map:
    actorJson, err := json.Marshal(actor)
    if err != nil {
        log.Fatal(err)
    }
    // We need to "cast" the returned slice of bytes as a string to properly print it:
    fmt.Println(string(actorJson))
}

// Output:
// {"age":57,"name":"Keanu Reeves"}

Before diving into how the json.Marshal() function works, let's first evaluate the actor map. It has the string type for keys and an empty interface type for values. The simplest explanation is that the empty interface{} type allows us to refer to any type in Go — it can hold a string, a pointer, a slice, a struct, etc.

Now let's go back to serialization. The json.Marshal() function takes the any type as an argument — an alias to an empty interface{}; then it traverses the any value recursively and returns a slice of bytes, containing the serialized data and an error, if it finds any during the encoding/marshaling process.

Remember that JSON only supports the string type as keys! If you try to use an int type as a key, the json.Marshal() function will convert the int key to a string type key, and for other types that can't be converted to a string, you will get the unsupported type error.

Serializing a map with indentation

The json.MarshalIndent() function works just like json.Marshal(); the only difference is that it takes two additional string arguments — prefix and indent.

Each JSON element in the output will start on a new line, beginning with the prefix, followed by one or more copies of indent, according to the indentation nesting:

...

func main() {
    ... // actor map declaration goes here

    actorJson, err := json.MarshalIndent(actor, "", "  ")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(actorJson))
}

// Output:
// {
//   "age": 57,
//   "name": "Keanu Reeves"
// }

An important detail is that when working with JSON strings, the most common indentation is to pass an empty prefix "" and two blank spaces " " as indents, just like the above example showcases.

If you want to play and see the output of different prefix and indent values, we've prepared this Go playground template for you!

Serializing a slice

The previous examples illustrated how to encode simple maps to JSON; now let's take a look at how to serialize a simple slice to JSON with the json.Marshal() function:

...

func main() {
    carBrands := []string{"Tesla", "BMW", "Toyota", "Ford"}

    carBrandsJson, err := json.Marshal(carBrands)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(carBrandsJson))
}

// Output:
// ["Tesla","BMW","Toyota","Ford"]

As you can see, the output is the same as when we serialized a map; the key difference is that the serialized contents are within brackets [...] instead of curly brackets {...}.

Remember that you can also use json.MarshalIndent() to serialize a slice to JSON with indentation!

Serializing nested JSON objects

So far we've only seen how to serialize simple JSON objects; however, in actual software development practice, we'll have to work with more complex JSON objects. For example, a map that contains a slice with several JSON objects:

...

func main() {
    books := map[string]interface{}{
        "books": []interface{}{
            map[string]interface{}{
                "isbn":   "9781491941959",
                "title":  "Introducing Go",
                "author": "Caleb Doxsey",
                "pages":  124,
            },
            ... // other books continue here
        },
    }

    booksJson, err := json.Marshal(books)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(booksJson))
}

// Output:
// {"books":[{"author":"Caleb Doxsey","isbn":"9781491941959","pages":124,"title":"Introducing Go"},{...}]}

As you can see, the serializing process is pretty straightforward: we can easily serialize the nested book map!

Now that you've seen multiple serialized outputs, you might've noticed that serialized map entries are sorted alphabetically. When encoding a Go map to JSON, the entries will be sorted alphabetically based on the map key! That's why author is the first key of the serialized output in the example above.

Deserializing JSON objects

Now it's time to perform the reverse operation, decoding JSON objects — also known as deserialization. To perform this operation, we can use the json.Unmarshal() function:

...

func main() {
    actorJson := `{"name": "Will Smith", "age":  53}`

    // Create the 'actor' map that will contain the decoded/unmarshaled JSON data:
    var actor map[string]interface{}

    // We need to "cast" the JSON object as slice of bytes '[]byte()' to properly decode it,
    // and pass a pointer '&actor' to decode the JSON object into the 'actor' variable:
    err := json.Unmarshal([]byte(actorJson), &actor)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(actor)
}

// Output:
// map[age:53 name:Will Smith]

Deserializing is also rather simple; however, there are two important details we need to know about:

  • First, we need to create and use an appropriate data type that can properly hold the values and follow the structure of the JSON object we plan to decode. For example, if the JSON object is a slice, we'll need to decode it to a slice type variable, and of course, if the JSON object is a map, we'll decode it to a map type variable with keys of the string type and an empty interface{} type for values.
  • The second important detail is that the json.Unmarshal() function takes the JSON object as a slice of bytes, and it requires a pointer to the variable we prepare to decode the JSON object to.

In the example above, you can see that we pass a pointer &actor to the json.Unmarshal() function. If we didn't pass a pointer, we would get the following error: json: Unmarshal(non-pointer map[string]interface {}).

By the way, you can also quickly convert a JSON object (either a slice or a map) to its equivalent data type in Go using the JSON to Golang Map tool: it will convert your JSON object into a Go interface Map representation (always work smarter, not harder!).

As a final note, remember that deserialized map entries are sorted alphabetically as well! Take notice that there are other special cases when working with JSON in Go that you might also want to know about.

Summary

In this topic, we've learned how to encode and decode JSON maps and slices in Go. Specifically, we've covered the following theory:

  • Encoding to JSON format is known as serialization, and decoding JSON objects is known as deserialization;
  • The encoding/json package has two functions for serializing: json.Marshal() and json.MarshalIndent() to indent and format the serialized output;
  • To deserialize JSON objects, we can use the json.Unmarshal() function;
  • The key part of the deserialization process is that to decode JSON, we need to use an appropriate data type that follows the same structure as the JSON object we plan to decode.

We've also mentioned that there are some other special cases when working with JSON in Go, the most notable one being that both serialized and deserialized map entries are sorted alphabetically.

No time to lose now! Let's go ahead and test our knowledge on encoding/decoding JSON in Go with some theory and coding tasks!

21 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo