9 minutes read

You're already familiar with the encoding/json package, and know how to use the json.Marshal() and json.Unmarshal() functions to encode and decode JSON maps and slices in Go. Besides that, we can also encode struct to JSON and decode JSON to struct. In fact, when working with JSON in Go, structs are the preferred data types, whereas we should only use maps with the empty interface{} type in case we're uncertain of the type of values a certain JSON object will contain.

In this topic, you'll learn how to encode a struct to JSON format and how to decode a JSON object to a struct in Go.

Serializing a struct

Suppose we want to serialize a simple struct User: we just need to initialize its fields and afterward use the json.Marshal() function on it:

package main

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

type User struct {
    ID        int
    IsActive  bool
    LastLogin time.Time
    email     string
}

func main() {
    usr := User{ID: 343, IsActive: true, LastLogin: time.Now(), email: "[email protected]"}

    usrJSON, err := json.Marshal(usr)
    if err != nil {
        log.Fatal(err)
    }
    // Remember to "cast" the returned slice of bytes as a 'string' to properly print it:
    fmt.Println(string(usrJSON))
}

// Output:
// {"ID":343,"IsActive":true,"LastLogin":"2022-04-12T19:54:26.1407768-05:00"}

As you can see, the process is the same as when serializing maps or slices. However, when working with structs, there is one very important detail we should know about:

The serialized output of the above example does not include the email field. This happens because email starts with a lowercase letter e, making it a private field of the User struct. As you might remember, private fields can't be accessed by other packages in our Go project, and since the JSON Encoder used by the json.Marshal() function is part of the encoding/json package, it won't be able to serialize any private struct fields!

Custom serialization

The previous section showcased the JSON serialized output of the User struct, and the names of the JSON keys all began with capital letters — ID, IsActive, LastLogin. Even though this is legal JSON, it doesn't follow the JavaScript convention of using camelCase for property names/keys.

However, this is not a problem! We can simply use struct tags to customize the JSON encoding of a struct, making the serialized fields follow camelCase naming conventions:

...

type User struct {      // this column has the struct tags
    ID        int       `json:"id"`
    IsActive  bool      `json:"isActive"`
    LastLogin time.Time `json:"lastLogin"`
    Email     string    `json:"email"`
}

func main() {
    usr := User{ID: 343, IsActive: true, LastLogin: time.Now(), Email: "[email protected]"}

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

// Output:
// {"id":343,"isActive":true,"lastLogin":"2022-04-12T19:56:01.7231225-05:00","email":[email protected]}

Optional struct tag directives

Take notice that there are optional directives we can add to the JSON struct tags:

  • omitempty omits a struct field when it contains a zero/default value;

  • we can use the hyphen - directive when we require a field in a struct to be public and accessible to other packages but prevent it from being encoded into JSON;

  • string forces the data in an individual field to be encoded as a string type in the resulting JSON.

Now, let's take a look at how to use these optional directives:

...

type User struct {
    ID        int       `json:"id,string"`
    IsActive  bool      `json:"isActive,omitempty"`
    LastLogin time.Time `json:"lastLogin,omitempty"`
    Email     string    `json:"-"`
}

func main() {
    // Initialize only 'ID' and 'Email' fields:
    usr := User{ID: 343, Email: "[email protected]"}

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

// Output:
// {"id":"343","lastLogin":"0001-01-01T00:00:00Z"}

In the above example, we use the string directive in the ID field, the omitempty directive in both IsActive and LastLogin fields, and the hyphen - directive in the Email field. Next, we initialize only two fields from the usr struct — ID and Email, this means that both IsActive and LastLogin fields will contain zero/default values, according to their type.

After encoding the usr struct with the json.Marshal() function, we can see the serialized output. ID has been encoded as a string type instead of an int type, and both IsActive and Email fields are not present in the output anymore, because we used the omitempty and hyphen - directives on them. However, the LastLogin field is still there, even though we passed the omitempty directive to it too! Why does this happen!?

This happens because the time.Time type is actually a struct type, and the omitempty directive never considers struct types to be empty. Instead, the string 0001-01-01T00:00:00Z will appear in the serialized output... this is one of the many surprises you can face when encoding JSON to Go structs!

Deserializing a JSON object to a struct

We've already seen how to serialize JSON to struct types, and now it's time to learn how to deserialize a JSON object to a Go struct using the json.Unmarshal() function.

For this example, we'll deserialize a nested error JSON object:

...

// Create the ErrJSONObject struct that will contain the decoded JSON data:
type ErrJSONObject struct {
    Errors []struct {
        Source struct {
            Pointer string `json:"pointer"`
        } `json:"source"`
        Detail string `json:"detail"`
    } `json:"errors"`
}

func main() {
    errJSON := `{
      "errors": [
        {
          "source": { "pointer": "" },
          "detail":  "Missing 'data' Member at document's top level."
        }
      ]
    }`

    var errObj ErrJSONObject // create an instance of the 'ErrJSONObject' struct type

    // Cast the JSON object as a slice of bytes to properly decode it,
    // and pass a pointer to the '&errObj' struct we plan to decode the JSON object to:
    err := json.Unmarshal([]byte(errJSON), &errObj)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v", errObj)
}

// Output:
// {Errors:[{Source:{Pointer:} Detail:Missing 'data' Member at document's top level.}]}

Deserializing to a struct in Go follows the same requirements as deserializing to a map or a slice:

  1. We need to create an appropriate struct type that can properly hold the values and follow the structure of the JSON object we want to decode;

  2. The json.Unmarshal() function takes the JSON object as a slice of bytes, and it requires a pointer to the variable we want to decode the JSON object to.

Creating an appropriate struct type can be challenging if the JSON response you plan to decode is lengthy or complex. But don't worry: if you are stuck trying to figure out the proper struct type to use, you can simply paste the JSON object you want to decode to the JSON-to-Go struct converter! It will automatically generate a Go struct type that follows the same structure as the JSON object.

Conclusion

In this topic, we've learned how to serialize and deserialize structured JSON in Go. Particularly, we've covered the following theory:

  • Encoding a struct to JSON format is very simple, we just need to use the json.Marshal() function to serialize it;

  • We can apply custom serialization to struct fields, using JSON struct tags with the following syntax — `json:"tagName"`;

  • There are optional directives we can add to JSON struct tags — omitempty, -, and string;

  • Decoding a JSON object to a struct in Go is simple as well. We only need to have an appropriate struct type that follows the same structure of the JSON object we want to decode and use the json.Unmarshal() function to deserialize it.

  • If you get stuck figuring out the proper struct type to use when trying to decode a JSON object, you can use the JSON-to-Go struct converter.

We've learned a lot, but there is still more to do! Let's test our newly acquired knowledge on serializing/deserializing structured JSON in Go with some theory and coding tasks!

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