Custom Unmarshallers in Go: Parsing Data Your Way

By Peter Leinonen on September 21, 2025

Custom Unmarshallers in Go: Parsing Data Your Way

When working with external APIs or data formats in Go, you'll often encounter situations where the default JSON unmarshalling behavior doesn't quite fit your needs. Maybe you're dealing with inconsistent date formats, need to transform string values to enums, or want to handle nested structures in a specific way. This is where custom unmarshallers shine.

The Problem

Consider this JSON response from an API:

{
  "user_id": "12345",
  "created_at": "2023-12-25T10:30:00Z",
  "status": "active",
  "preferences": "{\"theme\":\"dark\",\"notifications\":true}"
}

The challenges here are obvious: user_id is a string but you want an integer, preferences is a JSON string that needs parsing, and you might want status as a custom enum type rather than a plain string.

Implementing the json.Unmarshaler Interface

Go's json package provides the Unmarshaler interface that allows you to define custom unmarshalling logic:

type User struct {
    ID          int         `json:"user_id"`
    CreatedAt   time.Time   `json:"created_at"`
    Status      UserStatus  `json:"status"`
    Preferences Preferences `json:"preferences"`
}

type UserStatus int

const (
    StatusInactive UserStatus = iota
    StatusActive
    StatusSuspended
)

type Preferences struct {
    Theme         string `json:"theme"`
    Notifications bool   `json:"notifications"`
}

// Custom unmarshaller for UserStatus
func (s *UserStatus) UnmarshalJSON(data []byte) error {
    var status string
    if err := json.Unmarshal(data, &status); err != nil {
        return err
    }
    
    switch status {
    case "active":
        *s = StatusActive
    case "inactive":
        *s = StatusInactive
    case "suspended":
        *s = StatusSuspended
    default:
        return fmt.Errorf("unknown status: %s", status)
    }
    
    return nil
}

// Custom unmarshaller for Preferences
func (p *Preferences) UnmarshalJSON(data []byte) error {
    var prefsStr string
    if err := json.Unmarshal(data, &prefsStr); err != nil {
        return err
    }
    
    return json.Unmarshal([]byte(prefsStr), p)
}

Key Benefits

Type Safety: Convert loose string values into strongly-typed enums or custom types that provide compile-time guarantees.

Data Transformation: Handle inconsistent data formats from external sources without cluttering your business logic.

Validation: Implement validation rules directly in the unmarshalling process, catching bad data early.

Clean APIs: Keep your struct definitions clean and focused on your domain model rather than external data quirks.

Best Practices

Error Handling: Always return descriptive errors that help with debugging. Include the problematic value when possible.

Performance: For high-throughput applications, consider the performance impact of custom unmarshallers. Sometimes a two-step process (unmarshal to intermediate struct, then transform) can be more efficient.

Testing: Write comprehensive tests for your custom unmarshallers, including edge cases and malformed input.

Documentation: Document any special behavior or assumptions your unmarshallers make.

Conclusion

Custom unmarshallers are a powerful tool in Go's arsenal for handling real-world data parsing challenges. By implementing the json.Unmarshaler interface, you can maintain clean, type-safe code while gracefully handling messy external data formats. The key is striking the right balance between flexibility and performance based on your specific use case.

Next time you find yourself writing data transformation logic scattered throughout your codebase, consider whether a custom unmarshaller might be a cleaner solution.