Marshal structs the right way: Golang

31 Mar 2020 ⏱️ 5 min
Marshal structs the right way: Golang

A common problem which I have seen many of Golang beginner’s facing is handling structs in golang while marshalling to json etc. Even I came across this issue, so I finally decided to write about this. This is not exactly a problem, it is how golang works. But you see empty structs within empty structs in your JSON response and you are confused why is not null rather being empty. I got your back here, let’s explore this!

Note: This article assumes you have basic knowledge of structs in golang. If not, please read through this and this first.


The Problem

Let’s take a simple example to demonstrate the problem here. I’ll take an example of a User struct here with Name and Age property for a start.

package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	Name string
	Age  int
}

func main() {
	user := User{
		Name: "Mohit",
		Age:  23,
	}
	// Ignoring error for simplicity here (always handle errors)
	encodedData, _ := json.Marshal(user)
	fmt.Println(string(encodedData))

}

Output will be as it was expected -

{ "Name": "Mohit", "Age": 23 }

Let’s create 2 new users run above code.

user1 := User{
	Name: "Noob",
}

user2 := User{}

Guess the output now?

For user1 ->
{"Name":"Noob","Age":0}

For user2 ->
{"Name":"","Age":0}

Solution

Empty Structs

Confused?

Case1: We don’t input age in our struct but we get a value 0?

Reason: While marshalling, golang defaults Age to its type zero value ( in our case int whose zero value is 0)

Case2: We have an empty struct here.

Reason: The zero value of a struct is a struct with all fields set to their own zero values. That is why we see both string having “” and int having 0 value.

But you don’t want zero values in your struct, which is logical since you won’t want any extra data in your JSON response. Why increase data bytes passed through your APIs?

Solution:

This is the case where we use “omitempty” tag in json. From the official docs -

The "omitempty" option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.

Okay, this is exactly what we need right? Let’s update our user struct:

type User struct {
	Name string `json:"name,omitempty"`
	Age  int    `json:"age,omitempty"`
}

// Now run the above program with updated struct. Output will be -
// Case1 -
{"name":"Mohit"}
// Case2 -
{}

Awesome this works 😎

But, yeah there is always a but! 🤷‍♂️

If you read omitempty definition carefully, you realize it doesn’t works with empty struct !! Damnn! What if you have nested structs?

Hmm, Let’s solve this with extended example. Let’s add a Post struct in user -

package main

import (
	"encoding/json"
	"fmt"
)

type Post struct {
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
}

type User struct {
	Name string `json:"name,omitempty"`
	Age  int    `json:"age,omitempty"`
	Post Post   `json:"post,omitempty"`
}

func main() {
	user := User{
		Name: "Mohit",
	}
	encodedData, _ := json.Marshal(user)
	fmt.Println(string(encodedData))
}

Output for this is -

{ "name": "Mohit", "post": {} }

Even if I have omitempty, this doesn’t work. What now? Scroll up and read the definition of omitempty again. Check for term nil pointer.

Well, we can make a pointer to Post struct and our problem would be solved? Let’s try this!

type User struct {
	Name string `json:"name,omitempty"`
	Age  int    `json:"age,omitempty"`
	Post *Post   `json:"post,omitempty"` // making it pointer
}
// Output
{"name":"Mohit"} // Yaayy 🤘!!

But!! Yeah, told you there is always a but. 😛

Large Structs

What if your struct is huge. Say it has tens of 3 level nested structs? Would you manage pointer for each struct and making your code errorprone? No, no one likes do deal with loads of pointers.

The problem lies within json package itself. It handles zero value checks for everything but not for struct. Not sure, why though? Let me know if you know 😅

On some research and discussion, my teammate at work found out this issue - https://github.com/golang/go/issues/11939 which is exactly what we talk about here.

Also, there are custom json implementations which add this feature. Not sure if there are any performance issues in these libraries. The library I use for this purpose is https://github.com/clarketm/json. This works like charm for me.

I hope you solved your problem and learned something new. Try to implement this in your codebases 🚀 Do drop a 👍 and share it with your friends.

Liked the article? Consider supporting me ☕️


I hope you learned something new. Feel free to suggest improvements ✔️

I share regular updates and resources on Twitter. Let’s connect!

Keep exploring 🔎 Keep learning 🚀

Liked the content? Do support :)

Paypal - Mohit Khare
Buy me a coffee