Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
131: Custom JSON Marshaling with MarshalJSON
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
Readingstruct withSensorID (string)andValue (float64). - Implement the
MarshalJSONmethod for theReadingstruct. - Use the alias type pattern to ensure the
SensorIDis marshaled normally, but theValueis 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"}
There are no comments for now.