marshal and unmarshal json with golang, nil values, empty structs and more!

Go and JSON: Tags, Empty Strings and Nil Values

Recently, I have been spending a lot of time working with Go. While helping other developers I noticed that many struggled with encoding and decoding JSON. In this guide I cover a lot of the common issues developers run into and how to solve them.

Naming keys

In the following example I will define a struct and then marshal it using the encoding/json package.

package main

import (
	"encoding/json"
	"fmt"
)

type Item struct {
	Foo   bool
	Bar string
}

func main() {
	itemJson, _ := json.Marshal(&Item{Foo: true, Bar: "Test"})
	fmt.Println(string(itemJson))
}

The code above will output the following string.

{"Foo":true,"Bar":"Test"}

The keys Foo and Bar are using the same format as our struct. You may want these to be lowercase or different from your struct. To do that we can use struct tags like so.

type Item struct {
	Foo   bool `json:"foo"`
	Bar string `json:"bar_key"`
}

By only changing the struct and running the same code again we will get the following output.

{"foo":true,"bar_key":"Test"}

Excluding keys

We can exclude the keys by adding a - to our json tag in our struct. Any tags which use a - will be excluded when marshalling a struct. So let’s change one of our tags to - like this:

type Item struct {
	Foo   bool `json:"foo"`
	Bar string `json:"-"`
}

When we run our code again we will get the following output.

{"foo":true}

As you can see Bar is now excluded from the marshalled string. This is useful if you have structs that serve multiple purposes such as database queries and api responses.

Optional or empty keys when unmarshalling

In some situations you may send or receive JSON with optional keys which are sometimes present and sometimes not. The issue you may run into is that bools are false, ints are 0 and strings are "" when they are not set. I have seen some developers make huge workarounds to deal with this issue but there is a simple solution: pointers. Let’s take a look at an example without pointers.

package main

import (
	"encoding/json"
	"fmt"
)

type Item struct {
	Foo bool `json:"foo"`
	Bar int  `json:"bar"`
}

func main() {
	itemJson, _ := json.Marshal(&Item{})
	fmt.Println(string(itemJson))
	item := Item{}
	emptyJson := "{}"
	_ = json.Unmarshal([]byte(emptyJson), &item)
	fmt.Println(item.Bar, item.Foo)
}

In the above example we are doing two things. First, we marshal en empty Item struct without any values. Then we take an empty JSON object and Unmarshal this to a new Item struct and then print the values of Bar and Foo. The following is the output we get from running the above code:

{"foo":false,"bar":0}
0 false

Even though we did not set any values, Bar has a value of 0 and Foo has a value of false. This can be a problem with optional parameters because we don’t know if the data we received was actually set to a value or omitted. In the same way, we might end up sending values which we did not intend to send. Now, let’s try changing our struct to use pointers instead:

type Item struct {
	Foo *bool `json:"foo"`
	Bar *int  `json:"bar"`
}

If we run the same code but only adding * we get the following output:

{"foo":null,"bar":null}
<nil> <nil>

Now we have have nil values since we did not set anything. This allows us to check if a parameter was actually set to a value. For example, we might use the following code to check the value of item.Foo after unmarshalling.

if item.Foo == nil {
	// Foo was not set
} else if *item.Foo {
	// Foo was true
} else if !*item.Foo {
	// Foo was false
}

Conclusion

I hope this post has helped cover the common issues with Go and JSON. If I missed anything or made a mistake please let me know in the comments. Thank you for reading!

Got Something To Say?

Your email address will not be published. Required fields are marked *