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

Posted on April 28, 2023 [golang]

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:

go
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:

go
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:

  • Refactor the struct to have no inner fields:
  • go
    
    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.

  • Create a separate UnmarshalJSON method for the inner struct:
  • go
    
    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.