Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active April 10, 2024 08:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Integralist/6772c8861b1fb7dadc2a816e14e1fdf9 to your computer and use it in GitHub Desktop.
Save Integralist/6772c8861b1fb7dadc2a816e14e1fdf9 to your computer and use it in GitHub Desktop.
[Go API JSON Issues] #go #golang #json #api #omitempty

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.

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)) // {}
}
// 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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment