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 omitempty for optional fields to keep responses clean
  • Use json:"-" to hide sensitive fields
  • Use json.Decoder for HTTP request bodies instead of json.Unmarshal
  • Implement MarshalJSON for custom serialization of types like enums
  • Only exported (capitalized) fields are serialized - unexported fields are always ignored