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! :-)