Orso Labs

How to handle JSON fields with a default value different from the Go zero value (example: a bool with default value true), avoiding the inconveniences of pointers.

Default values in JSON with Golang

Imagine having a JSON object that allows the user to specify a message to send:

type Message struct {
    Text string `json:"text"`
}

This can be parsed with the classic snippet:

buf := []byte(`{"text": "hello"}`)
var msg Message
if err := json.Unmarshal(buf, &msg); err != nil {
    return err
}

Now imagine that the system will optionally append also performance data, not user-provided. We have to decide the default: should the performance data be appended to the user-supplied Text or not?

First reaction is just to add a boolean Append to the JSON object:

type Message struct {
    Text    string `json:"message"`
    Append  bool   `json:"append"`
}

But this causes the default value of Append to be the zero value of a boolean: false.

If we use the following test cases:

var inputs = []string{
    `{"text":"hi"}`,                  // <== "append" is missing: default value
    `{"text":"hi", "append": false}`, // <== "append": explicit value of false
    `{"text":"hi", "append": true}`,  // <== "append": explicit value of true
}

And the following test driver:

for _, in := range inputs {
    var msg Message
    if err := json.Unmarshal([]byte(in), &msg); err != nil {
        return err
    }
    fmt.Printf("%-32s Text:%s Append:%v\n", in, msg.Text, msg.Append)
}

We get:

--------json--------             --------msg--------
{"text":"hi"}                    {Text:hi Append:false}
{"text":"hi", "append": false}   {Text:hi Append:false}
{"text":"hi", "append": true}    {Text:hi Append:true}

If the default false for Append is adapted to our use case, we are done. But what do we do if we would like a default true ?

Take 1: bent backwards

If the Go language didn’t assist us at all, we could replace in the JSON format append with its negation, disable_append:

type Message struct {
    Text           string `json:"message"`
    DisableAppend  bool   `json:"disable_append"`
}

Here the default for DisableAppend would be false, which, in a contorted way, is equivalent to the default for Append (which we removed) to be true.

Can we do better?

Take 2: A nil pointer in Go means absence

If we use a pointer for the Append field

type Message struct {
    Text   string `json:"text"`
    Append *bool  `json:"append"`
}

and modify out test driver:

for _, in := range inputs {
    var msg Message
    if err := json.Unmarshal([]byte(in), &msg); err != nil {
        return err
    }
    if msg.Append == nil {
        // Same line as previous test driver
        fmt.Printf("%-32s Text:%s Append:%v\n", in, msg.Text, msg.Append)
    } else {
        // We can dereference without crashing
        fmt.Printf("%-32s Text:%s *Append:%v\n", in, msg.Text, *msg.Append)
    }
}

We get, for the same 3 cases:

--------json--------             --------msg--------
{"text":"hi"}                    Text:hi Append:<nil>   // pointer
{"text":"hi", "append": false}   Text:hi *Append:false  // *pointer
{"text":"hi", "append": true}    Text:hi *Append:true   // *pointer

So in this case we can make the difference between append being absent (when the pointer is nil) and append being set true or false by the user. Thus, when the pointer is nil, that is our default value, that we can interpret as we want.

This seems a progress, but using the pointer in this raw fashion would oblige us, as shown in the test driver, to always check for non-nil before actually looking at its value to avoid a crash. To avoid this, at the very minimum we should convert the nil value to its default value as soon as possible.

First idea that comes to mind is to add a method to Message to set the defaults:

func (msg *Message) SetDefaults() {
    if msg.Append == nil {
        val := true
        // cannot do: msg.Append = &true
        msg.Append = &val
    }
}

to be called just after the JSON parsing. We can also always safely dereference the pointer:

for _, in := range inputs {
    var msg MessageSetDefaults
    if err := json.Unmarshal([]byte(in), &msg); err != nil {
        return err
    }
    msg.SetDefaults()
    // We can always dereference without crashing
    fmt.Printf("%-32s Text:%s *Append:%v\n", in, msg.Text, *msg.Append)
}

This at least removes the nil:

--------json--------             --------msg--------
{"text":"hi"}                    Text:hi *Append:true    // *pointer
{"text":"hi", "append": false}   Text:hi *Append:false   // *pointer
{"text":"hi", "append": true}    Text:hi *Append:true    // *pointer

But there are still two unpleasant aspects:

  1. we must dereference the pointer on each usage
  2. we must remember to call msg.SetDefaults() just after the JSON parsing

Can we do better?

Take 3: pointers (but hidden) and aliasing

We go back to a Message struct without pointers:

type Message struct {
    Text   string `json:"text"`
    Append bool   `json:"append"`
}

And add method UnmarshalJSON to Message, making it implement interface Unmarshaler:

func (msg *Message) UnmarshalJSON(data []byte) error {
    type Alias Message
    type Aux struct {
        Append *bool `json:"append"` // We override the type of Append
        *Alias
    }
    aux := &Aux{Alias: (*Alias)(msg)}

    if err := json.Unmarshal(data, &aux); err != nil {
        return err
    }

    if aux.Append == nil {
        // Field "append" is not set: we want the default value to be true.
        msg.Append = true
    } else {
        // Field "append" is set: dereference and assign the value.
        msg.Append = *aux.Append
    }

    return nil
}

The alias type Alias Message is needed to avoid infinite recursion.

We can then simply call json.Unmarshal:

for _, in := range inputs {
    var msg Message
    if err := json.Unmarshal([]byte(in), &msg); err != nil {
        return err
    }
    fmt.Printf("%-32s Text:%s Append:%v\n", in, msg.Text, msg.Append)
}

and get:

{"text":"hi"}                    Text:hi Append:true
{"text":"hi", "append": false}   Text:hi Append:false
{"text":"hi", "append": true}    Text:hi Append:true

so we solved both problems of take 2:

  1. no pointers to dereference
  2. no need to call any function after json.Unmarshal

This is already pretty good. Can we do better?

Take 4: as simple as possible

As long as the types are simple, we can simplify even further, without using pointers at all: just set the default value before calling json.Unmarshal!

func (msg *Message) UnmarshalJSON(text []byte) error {
    type Alias Message
    aux := Alias{
        Append: true, // set the default value before parsing JSON
    }
    if err := json.Unmarshal(text, &aux); err != nil {
        return err
    }
    *msg = Message(aux)
    return nil
}

gives:

--------json--------             --------msg--------
{"text":"hi"}                    Text:hi Append:true
{"text":"hi", "append": false}   Text:hi Append:false
{"text":"hi", "append": true}    Text:hi Append:true

That’s it! :-)

References

#go #golang #idiomatic #json