Advanced JSON Customization
By using struct tags, adding whitespace and enveloping the data, we’ve been able to add quite a lot of customization to our JSON responses already. But what happens when these things aren’t enough, and you need the freedom to customize your JSON even more?
To answer this question, we first need to talk some theory about how Go handles JSON encoding behind the scenes. The key thing to understand is this:
When Go is encoding a particular type to JSON, it looks to see if the type has a MarshalJSON()
method implemented on it. If it has, then Go will call this method to determine how to encode it.
That language is a bit fuzzy, so let’s be more exact.
Strictly speaking, when Go is encoding a particular type to JSON it looks to see if the type satisfies the json.Marshaler
interface, which looks like this:
If the type does satisfy the interface, then Go will call its MarshalJSON()
method and use the []byte
slice that it returns as the encoded JSON value.
If the type doesn’t have a MarshalJSON()
method, then Go will fall back to trying to encode it to JSON based on its own internal set of rules.
So, if we want to customize how something is encoded, all we need to do is implement a MarshalJSON()
method on it which returns a custom JSON representation of itself in a []byte
slice.
Customizing the Runtime field
To help illustrate this, let’s look at a concrete example in our application.
When our Movie
struct is encoded to JSON, the Runtime
field (which is an int32
type) is currently formatted as a JSON number. Let’s change this so that it’s encoded as a string with the format "<runtime> mins"
instead. Like so:
There are a few ways we could achieve this, but a clean and simple approach is to create a custom type specifically for the Runtime
field, and implement a MarshalJSON()
method on this custom type.
To prevent our internal/data/movie.go
file from getting cluttered, let’s create a new file to hold the logic for the Runtime
type:
And then go ahead and add the following code:
There are two things I’d like to emphasize here:
If your
MarshalJSON()
method returns a JSON string value, like ours does, then you must wrap the string in double quotes before returning it. Otherwise it won’t be interpreted as a JSON string and you’ll receive a runtime error similar to this:We’re deliberately using a value receiver for our
MarshalJSON()
method rather than a pointer receiver likefunc (r *Runtime) MarshalJSON()
. This gives us more flexibility because it means that our custom JSON encoding will work on bothRuntime
values and pointers toRuntime
values. As Effective Go mentions:The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.
OK, now that the custom Runtime
type is defined, open your internal/data/movies.go
file and update the Movie
struct to use it like so:
Let’s try this out by restarting the API and making a request to the GET /v1/movies/:id
endpoint. You should now see a response containing the custom runtime value in the format "<runtime> mins"
, similar to this:
All in all, this is a pretty nice way to generate custom JSON. Our code is succinct and clear, and we’ve got a custom Runtime
type that we can use wherever and whenever we need it.
But there is a downside. It’s important to be aware that using custom types can sometimes be awkward when integrating your code with other packages, and you may need to perform type conversions to change your custom type to and from a value that the other packages understand and accept.
Additional Information
There are a couple of alternative approaches that you could take to reach the same end result here, and I’d like to quickly describe them and talk through their pros and cons. If you’re coding-along with the build, don’t make any of these changes (unless you’re curious, of course!).
Alternative #1 - Customizing the Movie struct
Instead of creating a custom Runtime
type, we could have implemented a MarshalJSON()
method on our Movie
struct and customized the whole thing. Like this:
Let’s quickly take stock of what’s going on here.
In the MarshalJSON()
method we create a new ‘anonymous’ struct and assign it to the variable aux
. This anonymous struct is basically identical to our Movie
struct, except for the fact that the Runtime
field has the type string
instead of int32
. We then copy all the values from the Movie
struct directly into the anonymous struct, except for the Runtime
value which we convert to a string in the format "<runtime> mins"
first. Then finally, we encode the anonymous struct to JSON — not the original Movie
struct — and return it.
It’s also worth pointing out that this is designed so the omitempty
directive still works with our custom encoding. If the value of the Runtime
field is zero, then the local runtime
variable will remain equal to ""
, which (as we mentioned earlier) is considered ‘empty’ for the purpose of encoding.
Alternative #2 - Embedding an alias
The downside of the approach above is that the code feels quite verbose and repetitive. You might be wondering: is there a better way?
To reduce duplication, instead of writing out all the struct fields long-hand it’s possible to embed an alias of the Movie
struct in the anonymous struct. Like so:
On one hand, this approach is nice because it drastically cuts the number of lines of code and reduces repetition. And if you have a large struct and only need to customize a couple of fields it can be a good option. But it’s not without some downsides.
In particular:
This technique feels like a bit of a ‘trick’, which hinges on the fact that newly defined types do not inherit methods. Although it’s still idiomatic Go, it’s more clever and less clear than the first approach. That’s not always a good trade-off… especially if you have new Gophers working with you on a codebase.
You lose granular control over the ordering of fields in the JSON response. In the above example, the
runtime
key will always now be the last item in the JSON object, like so:From a technical point of view, this shouldn’t matter as the JSON RFC states that JSON objects are “unordered collections of zero or more name/value pairs”. But it still might be dissatisfactory from an aesthetic or UI point of view, or be problematic if you need to maintain a precise field order for backward-compatibility purposes.