Skip to content

Instantly share code, notes, and snippets.

@dhermes
Created November 8, 2023 16:20
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 dhermes/c6a35eaba2c8d95dc58ca1de4775843c to your computer and use it in GitHub Desktop.
Save dhermes/c6a35eaba2c8d95dc58ca1de4775843c to your computer and use it in GitHub Desktop.
[2023-11-08] Subtleties of Go JSON marshaling and `null` / `nil`

Subtleties of Go JSON marshaling and null / nil

The TL;DR here is that if the standard encoding/json machinery in Go sees a null in a JSON field or a nil pointer in a Go field (of type *T), then it will not invoke the UnmarshalJSON() / MarshalJSON() on your type.

$ go run ./main.go
t1 = main.T{Shift:(*main.Point)(nil)} (nil stays nil, UnmarshalJSON() not invoked)
t2 = main.T{Shift:(*main.Point)(nil)} (see non-nil overwritten, UnmarshalJSON() not invoked)
::: Point{}.UnmarshalJSON() was invoked
t3 = main.T{Shift:(*main.Point)(0x140000a40f0)}; t3.Shift = &main.Point{X:8, Y:15} (see non-nil overwritten, UnmarshalJSON() invoked)
::: Point{}.MarshalJSON() was invoked
JSON(t4) = {"shift":{"x":3,"y":4}} (handle non-nil, MarshalJSON() invoked)
JSON(t5) = {"shift":null} (handle non-nil, MarshalJSON() not invoked)

Playground to give it a spin

package main
import (
"encoding/json"
"fmt"
"os"
)
type Point struct {
X int `json:"x"`
Y int `json:"y"`
}
func (p *Point) UnmarshalJSON(data []byte) error {
fmt.Println("::: Point{}.UnmarshalJSON() was invoked")
m := map[string]int{}
err := json.Unmarshal(data, &m)
if err != nil {
return err
}
p.X = m["x"]
p.Y = m["y"]
return nil
}
func (p Point) MarshalJSON() ([]byte, error) {
fmt.Println("::: Point{}.MarshalJSON() was invoked")
m := map[string]int{"x": p.X, "y": p.Y}
return json.Marshal(m)
}
type T struct {
Shift *Point `json:"shift"`
}
func run() error {
input1 := []byte(`{"shift": null}`)
t1 := T{}
err := json.Unmarshal(input1, &t1)
if err != nil {
return err
}
fmt.Printf("t1 = %#v (nil stays nil, UnmarshalJSON() not invoked)\n", t1)
////////////////////////////////////
input2 := []byte(`{"shift": null}`)
t2 := T{Shift: &Point{X: 3, Y: 4}}
err = json.Unmarshal(input2, &t2)
if err != nil {
return err
}
fmt.Printf("t2 = %#v (see non-nil overwritten, UnmarshalJSON() not invoked)\n", t2)
////////////////////////////////////
input3 := []byte(`{"shift": {"x":8, "y":15}}`)
t3 := T{Shift: &Point{X: 5, Y: 12}}
err = json.Unmarshal(input3, &t3)
if err != nil {
return err
}
fmt.Printf("t3 = %#v; t3.Shift = %#v (see non-nil overwritten, UnmarshalJSON() invoked)\n", t3, t3.Shift)
////////////////////////////////////
t4 := T{Shift: &Point{X: 3, Y: 4}}
asJSON, err := json.Marshal(t4)
if err != nil {
return err
}
fmt.Printf("JSON(t4) = %s (handle non-nil, MarshalJSON() invoked)\n", string(asJSON))
////////////////////////////////////
t5 := T{Shift: nil}
asJSON, err = json.Marshal(t5)
if err != nil {
return err
}
fmt.Printf("JSON(t5) = %s (handle non-nil, MarshalJSON() not invoked)\n", string(asJSON))
return nil
}
func main() {
err := run()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment