zoobzio December 26, 2025 Edit this page

Override Interfaces

The processor uses reflection to apply transforms based on struct tags. For performance-critical paths, implement override interfaces to bypass reflection entirely.

Available Interfaces

Five interfaces correspond to the five transform types:

// Called during Receive instead of reflection-based hashing
type Hashable interface {
    Hash(hashers map[HashAlgo]Hasher) error
}

// Called during Store instead of reflection-based encryption
type Encryptable interface {
    Encrypt(encryptors map[EncryptAlgo]Encryptor) error
}

// Called during Load instead of reflection-based decryption
type Decryptable interface {
    Decrypt(encryptors map[EncryptAlgo]Encryptor) error
}

// Called during Send instead of reflection-based masking
type Maskable interface {
    Mask(maskers map[MaskType]Masker) error
}

// Called during Send instead of reflection-based redaction
type Redactable interface {
    Redact() error
}

Implementation Example

type User struct {
    ID       string `json:"id"`
    Email    string `json:"email"`
    Password string `json:"password"`
    SSN      string `json:"ssn"`
    Token    string `json:"token"`
}

func (u User) Clone() User { return u }

// Bypass reflection for hashing on Receive
func (u *User) Hash(hashers map[cereal.HashAlgo]cereal.Hasher) error {
    if u.Password != "" {
        hasher, ok := hashers[cereal.HashSHA256]
        if !ok {
            return fmt.Errorf("missing sha256 hasher")
        }
        hashed, err := hasher.Hash([]byte(u.Password))
        if err != nil {
            return err
        }
        u.Password = hashed
    }
    return nil
}

// Bypass reflection for encryption on Store
func (u *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    if u.Email != "" {
        enc, ok := encryptors[cereal.EncryptAES]
        if !ok {
            return fmt.Errorf("missing AES encryptor")
        }
        ciphertext, err := enc.Encrypt([]byte(u.Email))
        if err != nil {
            return err
        }
        u.Email = base64.StdEncoding.EncodeToString(ciphertext)
    }
    return nil
}

// Bypass reflection for decryption on Load
func (u *User) Decrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    if u.Email != "" {
        enc, ok := encryptors[cereal.EncryptAES]
        if !ok {
            return fmt.Errorf("missing AES encryptor")
        }
        ciphertext, err := base64.StdEncoding.DecodeString(u.Email)
        if err != nil {
            return err
        }
        plaintext, err := enc.Decrypt(ciphertext)
        if err != nil {
            return err
        }
        u.Email = string(plaintext)
    }
    return nil
}

// Bypass reflection for masking on Send
func (u *User) Mask(maskers map[cereal.MaskType]cereal.Masker) error {
    if u.Email != "" {
        masker, ok := maskers[cereal.MaskEmail]
        if !ok {
            return fmt.Errorf("missing email masker")
        }
        u.Email = masker.Mask(u.Email)
    }
    if u.SSN != "" {
        masker, ok := maskers[cereal.MaskSSN]
        if !ok {
            return fmt.Errorf("missing SSN masker")
        }
        u.SSN = masker.Mask(u.SSN)
    }
    return nil
}

// Bypass reflection for redaction on Send
func (u *User) Redact() error {
    u.Token = "[REDACTED]"
    return nil
}

When to Use Override Interfaces

Use override interfaces when:

  1. Hot paths - Transforms happen in tight loops or high-frequency handlers
  2. Complex logic - Transform logic that doesn't fit the tag model
  3. Conditional transforms - Different behavior based on runtime state
  4. External dependencies - Transforms require services not available through setters

Performance Comparison

// Reflection-based (default)
BenchmarkProcessor_Store-8    500000    3200 ns/op    1024 B/op    12 allocs/op

// With Encryptable interface
BenchmarkProcessor_Store-8    800000    1800 ns/op     512 B/op     6 allocs/op

Actual gains depend on type complexity and number of transformed fields.

Partial Implementation

You can implement any subset of interfaces:

// Only bypass encryption, use reflection for everything else
func (u *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    // Custom encryption logic
}

// Only bypass masking
func (u *User) Mask(maskers map[cereal.MaskType]cereal.Masker) error {
    // Custom masking logic
}

Unimplemented interfaces fall back to reflection.

Interface Detection

The processor checks for interfaces at operation time on the cloned object:

// Store checks for Encryptable
clone := obj.Clone()
if enc, ok := any(&clone).(Encryptable); ok {
    enc.Encrypt(p.encryptors) // Uses interface
} else {
    p.applyEncrypt(&clone)    // Uses reflection
}

Combining with Tags

Override interfaces take precedence over tags. If you implement Encryptable, the store.encrypt tags are ignored:

type User struct {
    // This tag is ignored if Encryptable is implemented
    Email string `store.encrypt:"aes"`
}

func (u *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    // Your implementation handles Email (and any other fields)
}

This is all-or-nothing per interface. You can't mix reflection and interface for different fields of the same transform type.

Validation Bypass

When using override interfaces, Validate() doesn't check tag requirements for that transform type:

type User struct {
    Email string `store.encrypt:"custom"` // "custom" not registered
}

func (u *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    // Handles encryption manually
}

proc, _ := cereal.NewProcessor[User](json.New())
err := proc.Validate() // No error - Encryptable bypasses encrypt tag validation

Caveats

  1. Consistency - Your implementation must produce equivalent results to reflection
  2. Maintenance - Adding fields requires updating the method
  3. Testing - Test both paths to ensure equivalent behavior
  4. All-or-nothing - Can't mix reflection and interface for same transform type

Consider keeping reflection-based transforms for development and switching to override interfaces only after profiling confirms the need.

Example: Conditional Encryption

Override interfaces enable runtime decisions:

func (u *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    // Only encrypt if email is not already encrypted
    if !strings.HasPrefix(u.Email, "ENC:") {
        enc := encryptors[cereal.EncryptAES]
        ciphertext, _ := enc.Encrypt([]byte(u.Email))
        u.Email = "ENC:" + base64.StdEncoding.EncodeToString(ciphertext)
    }
    return nil
}

This isn't possible with tags alone.