JSON Tricks: "Slightly" Custom Marshaling

December 9, 2019

For more content like this, buy my in-progress eBook, Data Serialization in Go⁠, and get updates immediately as they are added!

Have you ever found yourself writing a custom JSON marshaler in Go, because you needed something only slightly different than what the standard JSON marshaler provides?

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.

  1. A single object, expressed as an array.

    type Tag string
    
    var x Tag = "foo"
    

    Desired output, as an array:

    ["foo"]
    
  2. 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:

  1. For the first example, the standard library produces:

    "foo"
    
  2. 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?


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .