« Back to Index

Go: API JSON with empy vs null fields issues

View original Gist on GitHub

Tags: #go #json #api

Go API JSON Issues.md

Reference: https://willnorris.com/2014/05/go-rest-apis-and-pointers/

If a struct field isn’t populated, and is marshalled to JSON, then the field’s zero value will be used (e.g. type string zero value == “”, type int zero value == 0).

You can use omitempty to prevent the field from being marshalled, but then you won’t know if the zero value was intentional or not (e.g. a user might want to set an type int field to zero or a type string field to an empty string).

To avoid that situation you need to have the field be set to a pointer of the type. This is because the zero value for a pointer is nil. This means if the field is nil then the field was never set but if it looks like a zero value for the type being pointed to, then you know it was set to the zero value intentionally.

Set null when Marshalling.go

package main

import (
	"encoding/json"
)

type Repository struct {
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	Private     *bool   `json:"private,omitempty"`
}

func (r *Repository) MarshalJSON() ([]byte, error) {
	type CustomRepository struct {
		Name        any `json:"name,omitempty"`
		Description any `json:"description,omitempty"`
		Private     any `json:"private,omitempty"`
	}

	cr := CustomRepository{}

	if r.Name != nil && *r.Name == "" {
		var name *string
		cr.Name = name
	} else if r.Name == nil {
		// This handles the case where you want the field omitted from the JSON response completely
	} else {
		cr.Name = r.Name
	}

	if r.Description != nil && *r.Description == "" {
		var description *string
		cr.Description = description
	} else if r.Description == nil {
		// This handles the case where you want the field omitted from the JSON response completely
	} else {
		cr.Description = r.Description
	}

	if r.Private != nil && *r.Private == false {
		var private *bool
		cr.Private = private
	} else if r.Private == nil {
		// This handles the case where you want the field omitted from the JSON response completely
	} else {
		cr.Private = r.Private
	}

	return json.Marshal(cr)
}

func main() {
	name := ""
	description := ""
	private := false

	// Explicitly set name to be a pointer to an empty string (e.g. I want this unset vs passing `nil` which means I've not set the field).
	r := &Repository{Name: &name}
	b, _ := json.Marshal(r)
	println(string(b)) // {"name":null}

	// Explicitly set name/description/private all to be pointers to their zero value (e.g. I want them all unset vs passing `nil` which means I've not set any of these fields).
	r = &Repository{Name: &name, Description: &description, Private: &private}
	b, _ = json.Marshal(r)
	println(string(b)) // {"name":null,"description":null,"private":null} <<< ISSUE: how do we make this work for someone who WANTS to set a bool type to `false` (rather than turn it to `null`)

	// Explicitly set actual values (e.g. I want these fields to be set to these values, not unset)
	name = "foo"
	description = "bar"
	private = true
	r = &Repository{Name: &name, Description: &description, Private: &private}
	b, _ = json.Marshal(r)
	println(string(b)) // {"name":"foo","description":"bar","private":true}

	// Explicitly set nothing
	r = &Repository{}
	b, _ = json.Marshal(r)
	println(string(b)) // {}
}

Unmarshal null correctly.go

// We use pointers to avoid a `null` being coerced into a type's zero value.
// e.g. `Bar` would otherwise contain an "" when, for something like Terraform, we need to know if it was set at all.
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"strings"
)

type Response struct {
	Foo *int    `json:"foo"`
	Bar *string `json:"bar"`
}

func main() {
	resp := strings.NewReader(`{"foo": 123, "bar": null}`)
	var r *Response
	if err := json.NewDecoder(resp).Decode(&r); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", r)
	fmt.Printf("%d\n", *r.Foo)
	fmt.Printf("%s\n", *r.Bar) // panic: runtime error: invalid memory address or nil pointer dereference
}