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

gogolang Updated Jul 8, 2025

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.

Related Posts

Unraveling the Mystery of Ignored Files with git check-ignore

In the world of version control, Git has become an indispensable tool for developers. One of its key features is the ability to selectively ignore certain files or directories with the help of the .gitignore file. This can be a real lifesaver when you need to exclude files that don't belong in your repository, like build artifacts, logs, or user-specific settings. However, sometimes it can be challenging to figure out why a particular file is being ignored. That's where the git check-ignore command comes in handy! In this blog post, we'll explore this powerful yet underutilized Git command and how it can help you understand your .gitignore configuration....

Introducing goqueuelite: Golang + SQLite queue

Introducing goqueuelite: Golang + SQLite queue

It finally happened! I am about to introduce my first proper open source project, it is called squeuelite and it is a Golang package that tries to fix the queue issue using SQLite only. The package can be found out github.com/risico/goqueuelite, check it out. The package is not production ready...

ZSH Alias Last Command and Alias + Persist commands

ZSH Alias Last Command and Alias + Persist commands

Introduction If you find yourself constantly running the same long commands on your terminal, setting up quick aliases can be a game-changer. This blog post will walk you through creating two handy ZSH functions that allow you to define and save aliases on-the-fly. With these, you can alias any