
For more content like this, buy my in-progress eBook, Data Serialization in Go, and get updates immediately as they are added!
Maybe the consumer of your JSON payload expects an array where you have a single item. Or maybe you need to nest your object one level deeper in your JSON than is used in your application.
In this article, I discuss a simple trick to help simplify what otherwise might be a complete reinvention of the wheel. In future articles, I will expand on this topic, so you may wish to subscribe below to be notified of these posts as they come.
If you are new JSON marshaling in Go, please start with JSON and Go from the official Go Blog. This article assumes you are already familiar with these concepts, as well as using custom json.Marshaler instances on your custom data types.
Examples
First let’s establish a few concrete examples. I’ll do this by showing you a Go struct, and the desired JSON output format. Then we’ll get into the details of making the desired output reality.
-
A single object, expressed as an array.
type Tag string var x Tag = "foo"
Desired output, as an array:
["foo"]
-
Deeply-nest an object
type Counts struct { Found int64 `json:"found"` NotFound int64 `json:"not_found"` } c := &Counts{Found: 156, NotFound: 83}
Desired output, as a member of the
attributes
object:{ "attributes": { "counts": { "found": 156, "not_found": 83 } } }
Why the standard library fails
Experienced Go developers will probably realize that the default output for these two examples are similar to, but not exactly, what we want:
-
For the first example, the standard library produces:
"foo"
-
And for the second:
{ "found": 156, "not_found": 83 }
The astute reader will notice that in both cases, what the standard library produces is part of our desired output, it’s just not enough.
Writing a custom marshaler
A custom JSON marshaler is fairly straight forward. For our first example, we might write something like this:
func (t Tag) MarshalJSON() ([]byte, error) {
return []byte("[" + t + "]")
}
And this would work in our example. But it’s far from perfect. For example,
what if our tag is foo"bar
. We’d suddenly produce invalid JSON:
["foo"bar"]
Or what if we need something a bit more complex than a simple string, like say a deeply nested object?
A better solution
What we’d really like to do, is marshal the original value using the standard library, then wrap it in an array. Something in theory a bit like this:
func (t Tag) MarshalJSON() ([]byte, error) {
jsonValue, err := json.Marshal(t)
if err != nil {
return nil, err
}
return []byte("[" + string(jsonValue) + "]")
}
With this approach, we’ll be sure that funny tags (like foo"bar
) are propery
escaped, and can use the same technique for arbitrarily complex data types, as
well.
There’s just one problem: This code creates an infinite loop!
The custom marshaler ends up calling itself, which again calls itself, ad infinitum.
Breaking the loop with a local type
To break this loop, we must not call json.Marshal
on the same data type on
which it was originally called. We can break this loop quite simply by defining
a local type, of the same underlying type as Tag
:
func (t Tag) MarshalJSON() ([]byte, error) {
type localTag Tag
jsonValue, err := json.Marshal(localTag(t))
if err != nil {
return nil, err
}
return []byte("[" + string(jsonValue) + "]")
}
Now the internal json.Marshal
call will be marshaling a different type,
localTag
which does not have a custom marshaler defined. The loop is broken!
Defining a type inside a function is perfectly valid in Go, and it prevents cluttering the package name space with once-used type names.
Another improvement
I hope you’re also bothered by the ugly return
line at the end of the method:
return []byte("[" + string(jsonValue) + "]")
This can also be done using the standard library. Just replace this custom
JSON building with another call to json.Marshal
as so:
return json.Marshal([]json.RawMessage{jsonValue})
The output should be identical, but the code is more robust and readable.
Another example
This time I’m not even going to attempt writing a custom marshaler from scratch for the second use-case. Instead I’ll just dive right in with a naïve “improved” approach:
func (c Counts) MarshalJSON() ([]byte, error) {
valueJson, err := json.Marshal(c)
if err != nil {
return nil, err
}
return json.Marshal(map[string]interface{}{
"attributes": map[string]interface{}{
"counts": json.RawMessage(valueJSON),
}
})
}
We still have the same problem as above: an infinite loop. And we could use the
same approach as before, by defining a local “copy” of our Counts
type.
Breaking the loop
Just as before, we can break the loop with a local type:
func (c Counts) MarshalJSON() ([]byte, error) {
type localCounts Counts
valueJson, err := json.Marshal(localCounts(c))
if err != nil {
return nil, err
}
return json.Marshal(map[string]interface{}{
"attributes": map[string]interface{}{
"counts": json.RawMessage(valueJSON),
}
})
}
Conclusion
A named local type is a simple, but powerful tool when building custom JSON marshalers (and unmarshalers). This shows one of the simplest, but by no means only, uses of such a pattern.
In future posts, I’ll expand on this basic principle, for much more powerful JSON handling techniques.
I’d love your feedback
Is this trick helpful to you? Please let me know in comments below.
Would you enjoy seeing other tips like this? What other challenges do you face with Go and JSON?