Handling Null JSON Arrays in Go

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 do this 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()
	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!

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.