Consider a situation where a third-party endpoint returns two different data structures at the same time. One possible solution to this issue is to use an interface; however, Golang also supports custom JSON Unmarshallers, which can effectively handle this problem.

For example, imagine an API that returns hotel data. The galleries field can appear in two ways:

Case 1: Flat Array 😎

json
{
  "galleries": [
    {
      "id": 1,
      "media": "/path/1.jpg"
    },
    {
      "id": 2,
      "media": "/path/2.jpg"
    }
  ]
}

Case 2: Object with Language Keys 👀

json
{
  "galleries": {
    "type-one": [
      {
        "id": 1,
        "media": "/path/en1.jpg"
      }
    ],
    "type-two": [
      {
        "id": 2,
        "media": "/path/fa1.jpg"
      }
    ]
  }
}

If we use a simple struct in Go, unmarshalling will fail for one of the two cases: 😍

go
type MyData struct {
    Galleries []GalleryItem `json:"galleries"` // works only for flat arrays
}
go
type MyData struct {
    Galleries Galleries `json:"galleries"` // works only for object form
}
go
type Galleries struct {
    Flat []GalleryItem // when galleries is a flat array
}

First Attempt: Using an interface{} 🥶

A common solution is to declare the field as interface{} and inspect its type at runtime:

go
type MyData struct {
    Galleries interface{
    }
    `json:"galleries"`
}

You could then use type assertions or map[string]interface{} checks to distinguish between the array and the object.

While this works, it pushes complexity downstream: every place where you consume Galleries must handle both cases, increasing the risk of nil checks and runtime errors.

A Better Way: Custom JSON Unmarshaller 🕵️‍♂️

Go allows us to attach custom parsing logic to our structs by implementing the UnmarshalJSON method. This way, we can centralize the logic and keep the rest of the codebase clean.

go
type MyData struct {
    Galleries Galleries `json:"galleries"`
}
type Galleries struct {
    Flat []GalleryItem // when galleries is a flat array
    Map map[string][]GalleryItem // when galleries is an object with arbitrary keys
}
type GalleryItem struct {
    ID int
    `json:"id"`
    Media string `json:"media"`
}
func (hg *MyData) UnmarshalJSON(data []byte) error {
    // Try flat slice
    var flat []GalleryItem
    if err := json.Unmarshal(data, &flat);
    err == nil {
        hg.Flat = flat return nil
    }
    // Try map[string][]GalleryItem
    var multi map[string][]GalleryItem
    if err := json.Unmarshal(data, &multi);
    err == nil {
        hg.Map = multi return nil
    }
    return fmt.Errorf("invalid galleries format")
}

Usage Example

go
import (
"encoding/json"
"testing"
)
func TestMyData_FlatGalleries(t *testing.T) {
    jsonData :=
    `{      "galleries": [        { "id": 1, "media": "/path/1.jpg"},        { "id": 2, "media": "/path/2.jpg"}      ]    }`
    var data MyData
    if err := json.Unmarshal([]byte(jsonData), &data);
    err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if len(data.Galleries.Flat) != 2 {
        t.Errorf("expected 2 flat galleries, got %d", len(data.Galleries.Flat))
    }
    if data.Galleries.Map != nil {
        t.Errorf("expected Map to be nil, got %+v", data.Galleries.Map)
    }
}
func TestMyData_MapGalleries(t *testing.T) {
    jsonData :=
    `{      "galleries": {        "type-one": [          { "id": 1, "media": "/path/en1.jpg"}        ],        "type-two": [          { "id": 2, "media": "/path/fa1.jpg"}        ]      }    }`
    var data MyData
    if err := json.Unmarshal([]byte(jsonData), &data);
    err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if data.Galleries.Flat != nil {
        t.Errorf("expected Flat to be nil, got %+v", data.Galleries.Flat)
    }
    if len(data.Galleries.Map) != 2 {
        t.Errorf("expected 2 keys in Map, got %d", len(data.Galleries.Map))
    }
    if data.Galleries.Map["type-one"][0].Media != "/path/en1.jpg" {
        t.Errorf("unexpected type-one media: %+v", data.Galleries.Map["type-one"])
    }
    if data.Galleries.Map["type-two"][0].Media != "/path/fa1.jpg" {
        t.Errorf("unexpected type-two media: %+v", data.Galleries.Map["type-two"])
    }
}

Conclusion

Dealing with inconsistent or variable JSON structures from third-party APIs is a common challenge in Go applications. When faced with a field like galleries that can appear as either a flat array or a nested object, relying on simple structs or interface{} leads to brittle code riddled with type assertions and nil checks—increasing complexity and the risk of runtime panics.

By implementing a custom UnmarshalJSON method, we can elegantly handle multiple JSON formats in a single, reusable way. This approach encapsulates the parsing logic within the type itself, ensuring that the rest of the application receives a consistent and safe data structure—free from unexpected nil values or type mismatches.

In this article, we demonstrated how to define a Galleries type capable of unmarshaling both array and object forms, using type-specific decoding logic that automatically adapts to the incoming data. With proper unit testing, this solution proves to be robust, maintainable, and idiomatic in Go.

Ultimately, custom unmarshalling empowers developers to write resilient code that gracefully handles real-world API inconsistencies — turning a potential source of bugs into a clean, predictable interface. When dealing with unpredictable JSON, UnmarshalJSON isn’t just a workaround; it’s a powerful tool for building reliable systems.

A message from our Founder

Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.

Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don’t receive any funding, we do this to support the community. ❤️

If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, Instagram. You can also subscribe to our weekly newsletter.

And before you go, don’t forget to clap and follow the writer️!