Skip to Content
Course content

131: Custom JSON Marshaling with MarshalJSON

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to countless developers—even senior ones moving to Go from other languages. You decide you need to change how a specific struct is represented in JSON, so you implement the Marshaler interface. You write your MarshalJSON() ([]byte, error) method, you return the struct you're currently in, and then you hit "run" only to have your program crash instantly with a stack overflow.

Thinking you can return the same struct in MarshalJSON

The misconception is that MarshalJSON is just a "hook" where you can tweak a value and then let the json package handle the final encoding. It isn't. When you call json.Marshal(myStruct), the package checks if myStruct implements MarshalJSON. If it does, it calls that method. If inside that method you call json.Marshal(myStruct) again, you've just created an infinite recursive loop.

Let's look at this disaster in action. Imagine we have a Product where we want to ensure the price always includes a currency symbol in the JSON output:

type Product struct {
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func (p Product) MarshalJSON() ([]byte, error) {
    // I just want to change how the price looks, 
    // so I'll just marshal the product again!
    return json.Marshal(p) // <--- BOOM: Infinite Recursion
}

The json.Marshal(p) call inside the method sees that p has a MarshalJSON method and calls it. Which calls json.Marshal(p). Which calls the method. Your stack is gone in milliseconds.

The Alias Type Trick: Breaking the Infinite Loop

To fix this, you need a way to tell the json package: "Marshal this data using the default logic, but ignore the MarshalJSON method I just wrote." The cleanest way to do this in Go is by creating a type alias inside the method.

When you define type Alias Product, the new Alias type has all the same fields as Product, but it has none of the methods. It's a blank slate. By casting your receiver to this alias, you break the recursion.

Here is how I actually handle this in production code:

type Product struct {
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func (p Product) MarshalJSON() ([]byte, error) {
    // Create an alias to avoid recursion
    type Alias Product
    
    // Cast p to Alias so it uses the default encoding logic
    // but we can still modify specific fields
    return json.Marshal(&struct {
        *Alias
        Price string `json:"price"`
    }{
        Alias: (*Alias)(&p),
        Price: fmt.Sprintf("$%.2f", p.Price),
    })
}

Notice what's happening here. I'm using an anonymous struct that embeds the Alias. This allows me to "override" only the Price field while keeping all other fields from the original Product intact. I'm essentially saying: "Give me everything from Product, but swap the float price for this formatted string."

It feels a bit like a magic trick the first time you see it, but it's the standard pattern in Go. Just remember: if you're implementing MarshalJSON and you need to call json.Marshal on the same data, you must strip the methods via an alias, or you're just building a very fast way to crash your server.




📋 Practical Task

Implement Custom Formatting for a Temperature Sensor

You are building an API for a weather station. You have a Reading struct that stores the temperature as a float64 in Celsius. However, the API specification requires the temperature to be returned as a string with a "°C" suffix (e.g., "23.5°C") instead of a raw number.

Your Task:

  • Create a Reading struct with SensorID (string) and Value (float64).
  • Implement the MarshalJSON method for the Reading struct.
  • Use the alias type pattern to ensure the SensorID is marshaled normally, but the Value is converted to a formatted string.
  • Ensure your implementation does not cause a stack overflow.

Expected JSON Output:
{"sensor_id": "living-room-1", "value": "21.4°C"}

Rating
0 0

There are no comments for now.

to be the first to leave a comment.