Use of omitzero json struct tag in Go
Go 1.24 introduced a new json struct tag called omitzero
. It handles a couple of scenarios that the existing omitempty
tag couldn’t. Let’s look at them one by one.
Marshalling empty time.Time struct fields
Let’s say we have the following customer struct
1type customer struct {
2 Name string `json:"name,omitempty"`
3 DOB time.Time `json:"dob,omitempty"`
4}
We have added the omitempty
json struct tag to both the Name
and DOB
fields with the expectation that if the field is not set, that particular field must be removed when the json
is created. Let’s write a program to test this out.
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "time"
8)
9
10type customer struct {
11 Name string `json:"name,omitempty"`
12 DOB time.Time `json:"dob,omitempty"`
13}
14
15func main() {
16 c := customer{}
17 cJson, err := json.Marshal(c)
18 if err != nil {
19 slog.Error("Unable to marshal customer", "error", err)
20 }
21 fmt.Println(string(cJson))
22}
Running the above program will print,
1{"dob":"0001-01-01T00:00:00Z"}
The omitempty
tag works well for string
types but not for time.Time
. Even when the DOB
was not specified when creating the struct, the output shows the zero value of time.Time
.
This becomes a hassle say when this json is passed to a web API call. The API doesn’t know whether the customer provided the DOB or not. The way this was handled in omitempty
was using pointers.
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "time"
8)
9
10type customer struct {
11 Name string `json:"name,omitempty"`
12 DOB *time.Time `json:"dob,omitempty"` //DOB changed to pointer
13}
14
15func main() {
16 c := customer{}
17 cJson, err := json.Marshal(c)
18 if err != nil {
19 slog.Error("Unable to marshal customer", "error", err)
20 }
21 fmt.Println(string(cJson))
22}
In the above program DOB is changed to a pointer. Since the pointer is nil
it is omitted in json. When the above program is run, it prints,
1{}
The json now doesn’t contain the DOB
field but there is a unnecessary pointer used to handle this scenario. This is fixed with the new omitzero
json tag.
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "time"
8)
9
10type customer struct {
11 Name string `json:"name,omitempty"`
12 DOB time.Time `json:"dob,omitzero"` //DOB json tag changed to omitzero and pointer removed
13}
14
15func main() {
16 c := customer{}
17 cJson, err := json.Marshal(c)
18 if err != nil {
19 slog.Error("Unable to marshal customer", "error", err)
20 }
21 fmt.Println(string(cJson))
22}
In the above program, DOB struct tag is changed to omitzero
and also the pointer is removed. Running this program prints,
1{}
The above output doesn’t have dob
in the json, just what we wanted!
Marshalling empty nested structs
The second case where omitempty
fails to handle zero values is when using nested structs. Consider the following program,
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "time"
8)
9
10type customer struct {
11 Name string `json:"name,omitempty"`
12 DOB time.Time `json:"dob,omitzero"`
13}
14
15type address struct {
16 Line1 string `json:"photo_id,omitempty"`
17 Line2 string `json:"location,omitempty"`
18}
19
20type SomeAPIRequest struct {
21 Customer customer `json:"customer,omitempty"`
22 Address address `json:"address,omitempty"` //omitempty set on nested struct
23}
24
25func main() {
26 c := customer{
27 Name: "John Doe",
28 }
29 apiRequest := SomeAPIRequest{
30 Customer: c,
31 }
32 apiRequestJson, err := json.Marshal(apiRequest)
33 if err != nil {
34 slog.Error("Unable to marshal api request", "error", err)
35 }
36 fmt.Println(string(apiRequestJson))
37}
In the above program, in line no. 22, the customer’s address is specified using another struct address
and it has omitempty
set. In line no. 26, the customer is created by setting the Name
but not setting the Address
. Let’s run the above program and check the output. Running the above program prints,
1{"customer":{"name":"John Doe"},"address":{}}
The output confirms that even though the address field is not set, it is present in the marshalled json. The solution to the problem is similar to the case of time.Time
and it is making Address
a pointer and keep using omitempty
. This issue is also fixed in the omitzero
tag. Let’s change the Address
field’s tag to omitzero
and see what happens.
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "time"
8)
9
10type customer struct {
11 Name string `json:"name,omitempty"`
12 DOB time.Time `json:"dob,omitzero"`
13}
14
15type address struct {
16 Line1 string `json:"photo_id,omitempty"`
17 Line2 string `json:"location,omitempty"`
18}
19
20type SomeAPIRequest struct {
21 Customer customer `json:"customer,omitempty"`
22 Address address `json:"address,omitzero"` //struct tag changed from omitempty to omitzero
23}
24
25func main() {
26 c := customer{
27 Name: "John Doe",
28 }
29 apiRequest := SomeAPIRequest{
30 Customer: c,
31 }
32 apiRequestJson, err := json.Marshal(apiRequest)
33 if err != nil {
34 slog.Error("Unable to marshal api request", "error", err)
35 }
36 fmt.Println(string(apiRequestJson))
37}
Running the program now prints the json without the address field.
1{"customer":{"name":"John Doe"}}
IsZero Method
The omitzero
tags also allows to define when a field can be considered as having a zero value. If the field has a isZero
method returning a bool
indicating whether the field is zero or not, omitzero
tag will use this method to determine whether the field’s value is a zero value or not. Let’s understand how this works by means of an example.
Consider the following struct type which indicates the retry configuration when calling a web API
1type configuration struct {
2 Timeout int // in seconds
3 Retries int
4}
The zero value 0
of retry could indicate that the API call must not be retried and hence having a custom zero value is helpful. We can define the IsZero
method on configuration
struct which will be used by omitzero
tag to determine whether configuration
is zero during json marshal.
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7)
8
9type configuration struct {
10 Timeout int
11 Retries int
12}
13
14type APIRequest struct {
15 Configuration configuration `json:"configuration,omitzero"`
16}
17
18func (c configuration) IsZero() bool {
19 if c.Timeout == 0 && c.Retries < 0 {
20 return true
21 }
22 return false
23}
24
25func main() {
26 apiRequest1 := APIRequest{
27 Configuration: configuration{
28 Retries: -1,
29 },
30 }
31 apiRequest1Json, err := json.Marshal(apiRequest1)
32 if err != nil {
33 slog.Error("Unable to marshal api request", "error", err)
34 }
35 fmt.Println("apiRequest1Json", string(apiRequest1Json))
36
37 apiRequest2 := APIRequest{
38 Configuration: configuration{},
39 }
40 apiRequest2Json, err := json.Marshal(apiRequest2)
41 if err != nil {
42 slog.Error("Unable to unmarshal api request", "error", err)
43 }
44 fmt.Println("apiRequest2Json", string(apiRequest2Json))
45}
The IsZero()
method in line no. 18 is used by omitzero
json tag to determine whether the configuration
struct is zero value of not. If Timeout
is 0 and Retries
is less than 0, then configuration
is considered to be zero value.
The Configuration
declared in line no. 27 has Retries
is set to -1
and Timeout
0 hence configuration is zero value and so it’s omitted during the json marshal in line no. 31.
The Configuration
declared in line no. 38 has Retries
and Timeout
set to 0 hence configuration is not zero value and so it’s included during json marshal. Running the above program prints
1apiRequest1Json {}
2apiRequest2Json {"configuration":{"Timeout":0,"Retries":0}}
Thanks for reading. I hope you liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter and LinkedIn. Have a good day.
If you’re looking for job opportunities in the Go programming language, check out gojobs.run, a search engine I own that specializes in indexing Go jobs.