Understanding the Unusual Behavior of Golang's Custom UnmarshalJSON Method with Inner and Outer Struct Fields

Introduction

In this blog post, we will discuss an interesting case in Golang where using a custom `UnmarshalJSON` method on a struct with both inner and outer fields results in only the inner fields being unmarshaled. We will look into why this occurs and suggest two alternative solutions to overcome this issue. Let's start by understanding the problem.

The Problem

Consider the following Go code with a struct named **`Person`** that has inner and outer fields:

 

type Name struct {
    First string `json:"first"`
    Last  string `json:"last"`
}

type Person struct {
    Name
    Age int `json:"age"`
}

Now, we want to implement a custom `UnmarshalJSON` method for the **`Person`** struct:

 

func (p *Person) UnmarshalJSON(data []byte) error {
    type alias Person
    aux := struct {
        *alias
        Age int `json:"age"`
    }{
        alias: (*alias)(p),
    }
    if err := json.Unmarshal(data, &aux); err != nil {
        return err
    }
    p.Age = aux.Age
    return nil
}

The expected behavior is that the custom `UnmarshalJSON` method should unmarshal both inner (Name) and outer (Age) fields. However, it turns out that only the inner fields are unmarshaled, and the outer field (Age) is ignored.

Why This Happens

The issue arises due to the use of the embedded struct Name in the Person struct. When the custom UnmarshalJSON method is called, it tries to unmarshal the JSON data into the embedded struct first. The outer field (Age) is then shadowed by the inner field with the same name in the auxiliary struct, which causes it to be ignored during the unmarshaling process.

Alternatives

To overcome this issue, we have two alternative solutions:

1. Refactor the struct to have no inner fields:

type Person struct {
    FirstName string `json:"first"`
    LastName  string `json:"last"`
    Age       int    `json:"age"`
}

By doing this, we avoid the issue of field shadowing and ensure all fields are unmarshaled correctly.

1. Create a separate UnmarshalJSON method for the inner struct:

func (n *Name) UnmarshalJSON(data []byte) error {
    type alias Name
    aux := &struct {
        *alias
    }{
        alias: (*alias)(n),
    }
    if err := json.Unmarshal(data, aux); err != nil {
        return err
    }
    return nil
}

By creating a separate UnmarshalJSON method for the inner struct (**`Name`**), we ensure that the JSON data is correctly unmarshaled for both the inner and outer fields.

Conclusion

In this blog post, we explored the peculiar behavior of Golang's custom UnmarshalJSON method when used with a struct containing both inner and outer fields. We discovered that only the inner fields are unmarshaled, and the outer fields are ignored due to field shadowing. To resolve this issue, we presented two alternative solutions - either refactor the struct to have no inner fields or create a separate `UnmarshalJSON` method for the inner struct.