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:
- we must dereference the pointer on each usage
- 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:
- no pointers to dereference
- 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! :-)