JSON and embedding
Prefer composition over inheritance, but with JSON both of them can get in the sea.
Did everyone else already know this? Why didn’t you tell me? I got very confused the other day with some apparently simple JSON encoding. Here’s a simplified version, showing marshalling a struct with an embedded struct inside it.
package main
import (
"encoding/json"
"fmt"
)
type Inner struct {
InnerField string `json:"inner_field"`
}
type Outer struct {
Inner
OuterField string `json:"outer_field"`
}
func main() {
val := Outer{
Inner: Inner {
InnerField: "inner",
},
OuterField: "outer",
}
data, err := json.MarshalIndent(val, "", " ")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(data))
}
Output is as follows.
{
"inner_field": "inner",
"outer_field": "outer"
}
Now, let’s say we want to implement a custom marshaller for Inner. This is a silly example - in the real world I was trying out easyjson looking for some performance gains.
func (i Inner) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"innnnnnerrFeild": %q}`, i.InnerField)), nil
}
What would you expect the output to be? I know I hoped it would be as follows.
{
"innnnnnerrFeild": "inner",
"outer_field": "outer"
}
But it turns out the output is as follows.
{
"innnnnnerrFeild": "inner"
}
Why is that? Well, Inner
implements json.Marshaller
, and because of embedding, so does Outer
. So when marshalling Outer
, the json library just calls the Inner
marshaller. So the entire JSON for Outer
is determined by Inner
’s MarshalJSON
. Entirely what you’d expect when you think about it, but completely surprising for me at least in this situation.
So, be extremely careful about using struct embedding with structs that are marshalled or unmarshalled.
How can we fix this? Well, if we add another field to Outer
that implements json.Marshaller
, then Outer
will no-longer implement json.Marshaller
as the implementation will be ambiguous. We can use struct{}
for this and it will take no room!
type marshalblock struct{}
func (marshalblock) MarshalJSON() ([]byte, error) { return nil, nil }
type Outer struct {
marshalblock
Inner
OuterField string `json:"outer_field"`
}
Here’s the output after this modification.
{
"inner_field": "inner",
"outer_field": "outer"
}
Unfortunately this also stops json from using the marshaller on Inner
. Which I think is a bug - but perhaps just another hint that embedding and JSON don’t mix.