A lovely picture of the sea
A lovely picture of the sea

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.