Go’s JSON package (encoding/json) provides straightforward capabilities for marshaling and unmarshaling data structures. However, certain scenarios require customization, such as handling non-standard JSON formats or manipulating data during these transformations. Here, we'll explore how to implement custom marshaling and unmarshaling logic.

Marshaling and Unmarshaling Basics

Let's start with the basics. Marshaling is the process of converting Go data structures to JSON. Unmarshaling is the reverse, converting JSON data into Go data structures.

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string
    Age  int
}

func main() {
    // Marshaling: Go struct to JSON
    user := User{Name: "Alice", Age: 30}
    jsonData, _ := json.Marshal(user)
    fmt.Println(string(jsonData))

    // Unmarshaling: JSON to Go struct
    jsonString := `{"Name":"Bob","Age":25}`
    var newUser User
    json.Unmarshal([]byte(jsonString), &newUser)
    fmt.Println(newUser)
}

Custom Marshaling

To implement custom marshaling, a data type must implement the json.Marshaler interface, which requires the MarshalJSON() method.

package main

import (
    "encoding/json"
    "fmt"
)

type Employee struct {
    Name     string
    Salary   int
    Position string
}

func (e Employee) MarshalJSON() ([]byte, error) {
    type Alias Employee // create an alias to avoid recursive calls
    return json.Marshal(&struct {
        Position string `json:"role"`
        Alias
    }{
        Position: e.Position,
        Alias:    (Alias)(e),
    })
}

func main() {
    emp := Employee{Name: "Charlie", Salary: 50000, Position: "Engineer"}
    jsonData, _ := json.Marshal(emp)
    fmt.Println(string(jsonData)) // Output: {"Name":"Charlie","Salary":50000,"role":"Engineer"}
}

In this example, we changed the field name Position to role in the JSON output, showcasing a field renaming customization.

Custom Unmarshaling

To enact custom unmarshaling behavior, implement the json.Unmarshaler interface, which involves the UnmarshalJSON() method.

package main

import (
    "encoding/json"
    "fmt"
)

type Account struct {
    ID     int    `json:"id"`
    Name   string `json:"name"`
    Active bool   `json:"active"`
}

func (a *Account) UnmarshalJSON(data []byte) error {
    var temp struct {
        ID     int    `json:"id"`
        Name   string `json:"name"`
        Status string `json:"status"`
    }
    if err := json.Unmarshal(data, &temp); err != nil {
        return err
    }
    a.ID = temp.ID
    a.Name = temp.Name
    a.Active = (temp.Status == "active") // Converts status string to boolean
    return nil
}

func main() {
    jsonString := `{"id":1,"name":"Roy","status":"active"}`
    var acc Account
    json.Unmarshal([]byte(jsonString), &acc)
    fmt.Println(acc) // Output: {1 Roy true}
}

This example demonstrates custom behavior during unmarshaling, where a string field status from the JSON is converted into a boolean field Active in Go.

Combining Both Techniques

You can combine both custom marshaling and unmarshaling techniques in the same struct to handle complex data transformation requirements. This allows for full control over the JSON representation and parsing logic.

Customize these methods according to your application's needs to neatly control how data is transformed in and out of your Go programs.