Marshaling is the process of converting a Go struct into a byte format (usually JSON) that can be transmitted over the network or stored. Unmarshaling is the reverse - taking raw bytes and populating a Go struct. If you are building APIs in Go, you do this constantly.
Basic Marshaling
The encoding/json package provides json.Marshal and json.Unmarshal for converting between Go structs and JSON.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string
Email string
Age int
}
func main() {
user := User{
Name: "Mohit",
Email: "mohit@example.com",
Age: 25,
}
// Marshal: struct -> JSON bytes
data, err := json.Marshal(user)
if err != nil {
panic(err)
}
fmt.Println(string(data))
// {"Name":"Mohit","Email":"mohit@example.com","Age":25}
}
Notice that the JSON keys match the Go field names exactly, including capitalization. In most APIs, you want lowercase keys. That is where struct tags come in.
Struct Tags
Struct tags let you control how fields are serialized. The json tag is the most common:
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
// Now produces: {"name":"Mohit","email":"mohit@example.com","age":25}
The omitempty Option
The omitempty option excludes a field from the JSON output if it has its zero value (empty string, 0, nil, false, empty slice):
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age,omitempty"`
Address string `json:"address,omitempty"`
}
user := User{Name: "Mohit", Email: "mohit@example.com"}
// Produces: {"name":"Mohit","email":"mohit@example.com"}
// Age (0) and Address ("") are omitted
This is useful for optional fields. Without omitempty, every field appears in the output with its zero value, which clutters the JSON and can confuse API consumers.
Ignoring Fields
Use json:"-" to exclude a field from serialization entirely:
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"` // never include in JSON output
}
This is critical for sensitive data. You should never accidentally serialize passwords, tokens, or internal IDs into API responses.
Unmarshaling JSON
Unmarshaling parses JSON into a Go struct:
jsonStr := `{"name":"Mohit","email":"mohit@example.com","age":25}`
var user User
err := json.Unmarshal([]byte(jsonStr), &user)
if err != nil {
panic(err)
}
fmt.Println(user.Name) // Mohit
Important: the target variable must be a pointer (&user). Unmarshal needs to modify the value, and Go passes by value, so a pointer is required.
Unknown fields in the JSON are silently ignored by default. If the JSON has a field "phone" but your struct does not, no error occurs. The field is simply discarded.
Working with json.Decoder
For reading JSON from an HTTP request body (an io.Reader), use json.Decoder instead of json.Unmarshal:
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var user User
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// use user...
}
json.Decoder reads directly from the stream without buffering the entire body into memory first. For large payloads, this is more efficient.
Custom Marshalers
Sometimes you need custom serialization logic. Implement the json.Marshaler interface:
type Status int
const (
StatusActive Status = 1
StatusInactive Status = 2
)
func (s Status) MarshalJSON() ([]byte, error) {
var str string
switch s {
case StatusActive:
str = "active"
case StatusInactive:
str = "inactive"
default:
str = "unknown"
}
return json.Marshal(str)
}
type User struct {
Name string `json:"name"`
Status Status `json:"status"`
}
// Produces: {"name":"Mohit","status":"active"}
// instead of: {"name":"Mohit","status":1}
This is a common pattern for enums. Internally you use integers for performance and type safety, but the API returns human-readable strings.
Nested Structs and Slices
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type User struct {
Name string `json:"name"`
Address Address `json:"address"`
Languages []string `json:"languages"`
}
user := User{
Name: "Mohit",
Address: Address{City: "Bangalore", Country: "India"},
Languages: []string{"Go", "JavaScript", "Python"},
}
// Produces:
// {
// "name": "Mohit",
// "address": {"city": "Bangalore", "country": "India"},
// "languages": ["Go", "JavaScript", "Python"]
// }
Pretty Printing
For debugging or logging, use json.MarshalIndent:
data, err := json.MarshalIndent(user, "", " ")
// Produces nicely formatted JSON with 2-space indentation
Key Takeaways
- Always use struct tags to control JSON key names
- Use
omitemptyfor optional fields to keep responses clean - Use
json:"-"to hide sensitive fields - Use
json.Decoderfor HTTP request bodies instead ofjson.Unmarshal - Implement
MarshalJSONfor custom serialization of types like enums - Only exported (capitalized) fields are serialized - unexported fields are always ignored