One of the most frustrating things with Go is how it handles empty arrays when encoding JSON. Rather than returning what is traditionally expected, an empty array, it instead returns null. For example, the following:
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
// Bag holds items | |
type Bag struct { | |
Items []string | |
} | |
// PrintJSON converts payload to JSON and prints it | |
func PrintJSON(payload interface{}) { | |
response, _ := json.Marshal(payload) | |
fmt.Printf("%s\n", response) | |
} | |
func main() { | |
bag1 := Bag{} | |
PrintJSON(bag1) | |
} |
Outputs:
{"Items":null}
This occurs because of how the json package handles nil slices:
Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice encodes as the null JSON value.
There are some proposals to amend the json package to handle nil slices:
But, as of this writing, these proposals have not been accepted. As such, in order to overcome the problem of null arrays, we have to set nil slices to empty slices. See the addition of line 22 in the following:
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
// Bag holds items | |
type Bag struct { | |
Items []string | |
} | |
// PrintJSON converts payload to JSON and prints it | |
func PrintJSON(payload interface{}) { | |
response, _ := json.Marshal(payload) | |
fmt.Printf("%s\n", response) | |
} | |
func main() { | |
bag1 := Bag{} | |
bag1.Items = make([]string, 0) | |
PrintJSON(bag1) | |
} |
Changes the output to:
{"Items":[]}
However, this can become quite tedious to do everywhere there can potentially be a nil slice. Is there a better way to do this? Let’s see!
Method 1: Custom Marshaler
According to the Go json docs:
Marshal traverses the value v recursively. If an encountered value implements the Marshaler interface and is not a nil pointer, Marshal calls its MarshalJSON method to produce JSON.
So, if we implement the Marshaler
interface:
// Marshaler is the interface implemented by types that
// can marshal themselves into valid JSON.
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
Our MarshalJSON()
will be called when encoding the data. See the additional MarshalJSON()
at line 14:
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
// Bag holds items | |
type Bag struct { | |
Items []string | |
} | |
// MarshalJSON initializes nil slices and then marshals the bag to JSON | |
func (b Bag) MarshalJSON() ([]byte, error) { | |
type Alias Bag | |
a := struct { | |
Alias | |
}{ | |
Alias: (Alias)(b), | |
} | |
if a.Items == nil { | |
a.Items = make([]string, 0) | |
} | |
return json.Marshal(a) | |
} | |
// PrintJSON converts payload to JSON and prints it | |
func PrintJSON(payload interface{}) { | |
response, _ := json.Marshal(payload) | |
fmt.Printf("%s\n", response) | |
} | |
func main() { | |
bag1 := Bag{} | |
PrintJSON(bag1) | |
} |
This would then output:
{"Items":[]}
The Alias
on line 15 is required to prevent an infinite loop when calling json.Marshal()
.
Method 2: Dynamic Initialization
Another way to handle nil slices is to use the reflect package to dynamically inspect every field of a struct; if it’s a nil slice, replace it with an empty slice. See NilSliceToEmptySlice()
on line 15:
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"reflect" | |
) | |
// Bag holds items | |
type Bag struct { | |
Items []string | |
} | |
// NilSliceToEmptySlice recursively sets nil slices to empty slices | |
func NilSliceToEmptySlice(inter interface{}) interface{} { | |
// original input that can't be modified | |
val := reflect.ValueOf(inter) | |
switch val.Kind() { | |
case reflect.Slice: | |
newSlice := reflect.MakeSlice(val.Type(), 0, val.Len()) | |
if !val.IsZero() { | |
// iterate over each element in slice | |
for j := 0; j < val.Len(); j++ { | |
item := val.Index(j) | |
var newItem reflect.Value | |
switch item.Kind() { | |
case reflect.Struct: | |
// recursively handle nested struct | |
newItem = reflect.Indirect(reflect.ValueOf(NilSliceToEmptySlice(item.Interface()))) | |
default: | |
newItem = item | |
} | |
newSlice = reflect.Append(newSlice, newItem) | |
} | |
} | |
return newSlice.Interface() | |
case reflect.Struct: | |
// new struct that will be returned | |
newStruct := reflect.New(reflect.TypeOf(inter)) | |
newVal := newStruct.Elem() | |
// iterate over input's fields | |
for i := 0; i < val.NumField(); i++ { | |
newValField := newVal.Field(i) | |
valField := val.Field(i) | |
switch valField.Kind() { | |
case reflect.Slice: | |
// recursively handle nested slice | |
newValField.Set(reflect.Indirect(reflect.ValueOf(NilSliceToEmptySlice(valField.Interface())))) | |
case reflect.Struct: | |
// recursively handle nested struct | |
newValField.Set(reflect.Indirect(reflect.ValueOf(NilSliceToEmptySlice(valField.Interface())))) | |
default: | |
newValField.Set(valField) | |
} | |
} | |
return newStruct.Interface() | |
case reflect.Map: | |
// new map to be returned | |
newMap := reflect.MakeMap(reflect.TypeOf(inter)) | |
// iterate over every key value pair in input map | |
iter := val.MapRange() | |
for iter.Next() { | |
k := iter.Key() | |
v := iter.Value() | |
// recursively handle nested value | |
newV := reflect.Indirect(reflect.ValueOf(NilSliceToEmptySlice(v.Interface()))) | |
newMap.SetMapIndex(k, newV) | |
} | |
return newMap.Interface() | |
case reflect.Ptr: | |
// dereference pointer | |
return NilSliceToEmptySlice(val.Elem().Interface()) | |
default: | |
return inter | |
} | |
} | |
// PrintJSON converts payload to JSON and prints it | |
func PrintJSON(payload interface{}) { | |
newPayload := NilSliceToEmptySlice(payload) | |
response, _ := json.Marshal(newPayload) | |
fmt.Printf("%s\n", response) | |
} | |
func main() { | |
bag1 := Bag{} | |
PrintJSON(bag1) | |
} |
This would then output:
{"Items":[]}
Review
The drawback of the custom marshaler is you have to write one for every struct that has slices. Because it’s custom, though, it can target the specific field that might be a nil slice.
The dynamic initialization approach is definitely slower because every field of the struct needs to be inspected to see if it needs to be replaced. However, this approach works well if you have lots of structs with slices and few places where you call json.Marshal()
.
Which approach would you use? Let me know in the comments below!
Top comments (0)