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:
- Hot paths - Transforms happen in tight loops or high-frequency handlers
- Complex logic - Transform logic that doesn't fit the tag model
- Conditional transforms - Different behavior based on runtime state
- 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
- Consistency - Your implementation must produce equivalent results to reflection
- Maintenance - Adding fields requires updating the method
- Testing - Test both paths to ensure equivalent behavior
- 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.