Concepts
Codec vs Processor
Codec is a simple interface for marshaling and unmarshaling:
type Codec interface {
ContentType() string
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
}
Use a codec directly when you need basic encoding without transforms.
ProcessorT provides boundary-aware transforms. Optionally wraps a codec for serialization:
type Processor[T Cloner[T]] struct {
// ...
}
// Primary API: T -> T
func (p *Processor[T]) Receive(ctx context.Context, obj T) (T, error)
func (p *Processor[T]) Load(ctx context.Context, obj T) (T, error)
func (p *Processor[T]) Store(ctx context.Context, obj T) (T, error)
func (p *Processor[T]) Send(ctx context.Context, obj T) (T, error)
// Secondary codec-aware API
func (p *Processor[T]) Decode(ctx context.Context, data []byte) (*T, error)
func (p *Processor[T]) Read(ctx context.Context, data []byte) (*T, error)
func (p *Processor[T]) Write(ctx context.Context, obj *T) ([]byte, error)
func (p *Processor[T]) Encode(ctx context.Context, obj *T) ([]byte, error)
Use a processor when data needs different treatment at different boundaries.
The Four Boundaries
Data flows through four boundaries:
External World Your Application Storage
│ │ │
│ ──── Receive ────> │ │
│ │ ──── Store ────> │
│ │ <──── Load ───── │
│ <──── Send ────── │ │
│ │ │
| Boundary | Operation | Transforms | Example |
|---|---|---|---|
| Receive | Receive() | receive.hash | Hash passwords from API requests |
| Load | Load() | load.decrypt | Decrypt fields from database |
| Store | Store() | store.encrypt | Encrypt fields for database |
| Send | Send() | send.mask, send.redact | Mask PII in API responses |
ClonerT Constraint
All types used with Processor[T] must implement Cloner[T]:
type Cloner[T any] interface {
Clone() T
}
This ensures the original value is never modified during transforms. All four operations clone before transforming.
Deep Copy Requirement
WARNING: Clone must return a true deep copy. A shallow copy (return u) is only safe for types with no reference fields.
| Field Type | Deep Copy Required? |
|---|---|
string, int, bool, etc. | No (copied by value) |
[]T (slice) | Yes |
map[K]V | Yes |
*T (pointer) | Yes |
| Struct with any of the above | Yes |
Simple Value Types
For types with only primitive fields:
type User struct {
ID string
Name string
Age int
}
func (u User) Clone() User { return u } // Safe: all fields are values
Types with Reference Fields
For types with slices, maps, or pointers:
type User struct {
ID string
Tags []string // Slice
Settings map[string]string // Map
Profile *Profile // Pointer
}
func (u User) Clone() User {
clone := User{ID: u.ID}
// Deep copy slice
if u.Tags != nil {
clone.Tags = make([]string, len(u.Tags))
copy(clone.Tags, u.Tags)
}
// Deep copy map
if u.Settings != nil {
clone.Settings = make(map[string]string, len(u.Settings))
for k, v := range u.Settings {
clone.Settings[k] = v
}
}
// Deep copy pointer
if u.Profile != nil {
p := *u.Profile
clone.Profile = &p
}
return clone
}
Verification
Test that modifying the clone does not affect the original:
func TestClone(t *testing.T) {
original := User{Tags: []string{"admin"}}
clone := original.Clone()
clone.Tags[0] = "user"
if original.Tags[0] != "admin" {
t.Error("Clone modified original")
}
}
Boundary Tags
Struct tags declare which transforms apply at which boundaries:
type User struct {
// Hash on receive (incoming data)
Password string `receive.hash:"sha256"`
// Encrypt on store, decrypt on load
Email string `store.encrypt:"aes" load.decrypt:"aes"`
// Mask on send (outgoing data)
SSN string `send.mask:"ssn"`
// Redact on send
Token string `send.redact:"[HIDDEN]"`
}
Tag Format
boundary.operation:"algorithm"
| Boundary | Operations |
|---|---|
receive | hash |
load | decrypt |
store | encrypt |
send | mask, redact |
Tag Values
Tag values reference registered algorithms:
// Tag value "aes" maps to EncryptAES
Email string `store.encrypt:"aes"`
// Registration
proc.SetEncryptor(cereal.EncryptAES, enc)
Built-in algorithms:
| Type | Constants |
|---|---|
| Encryption | EncryptAES, EncryptRSA, EncryptEnvelope |
| Hashing | HashSHA256, HashSHA512, HashArgon2, HashBcrypt |
| Masking | MaskEmail, MaskSSN, MaskPhone, MaskCard, MaskIP, MaskUUID, MaskIBAN, MaskName |
Configuration via Setters
Processors are configured with setter methods:
proc, _ := cereal.NewProcessor[User]()
// Configure encryption
enc, _ := cereal.AES(key)
proc.SetEncryptor(cereal.EncryptAES, enc)
// Configure custom hasher
proc.SetHasher(cereal.HashArgon2, customArgon2)
// Configure custom masker
proc.SetMasker(cereal.MaskEmail, customEmailMasker)
Setters return the processor for chaining:
proc.SetEncryptor(cereal.EncryptAES, aesEnc).
SetEncryptor(cereal.EncryptRSA, rsaEnc).
SetHasher(cereal.HashArgon2, argon2)
Validation
Validation runs automatically on the first operation. If a required handler is missing, the operation returns an error:
proc, _ := cereal.NewProcessor[User]()
// Forgot to configure encryptor...
result, err := proc.Store(ctx, user)
// err: missing encryptor for algorithm "aes" (field Email)
For fail-fast behavior at startup, call Validate() explicitly:
proc, _ := cereal.NewProcessor[User]()
proc.SetEncryptor(cereal.EncryptAES, enc)
if err := proc.Validate(); err != nil {
log.Fatal(err) // Catch configuration errors immediately
}
Validation checks that all algorithms referenced in tags have registered handlers. Types implementing override interfaces (Encryptable, Decryptable, Hashable, Maskable) bypass validation for their respective transform types.
Sentinel Integration
The processor uses sentinel for struct metadata. Field plans are built once per type and cached globally:
// First call for User builds and caches field plans
proc1, _ := cereal.NewProcessor[User]()
// Subsequent calls reuse cached plans (no reflection overhead)
proc2, _ := cereal.NewProcessor[User]()
// Each processor has independent configuration
proc1.SetEncryptor(cereal.EncryptAES, enc1)
proc2.SetEncryptor(cereal.EncryptAES, enc2)
This design provides:
- Shared plans: Reflection happens once per type, regardless of how many processors are created
- Independent state: Each processor maintains its own encryptors, hashers, and maskers
- Safe key rotation: Configure different keys per processor without affecting others
Test Isolation
For tests that need to verify plan building, ResetPlansCache() clears the internal cache:
func TestSomething(t *testing.T) {
cereal.ResetPlansCache()
defer cereal.ResetPlansCache()
proc, _ := cereal.NewProcessor[User]()
// ...
}
Most tests don't need this—processors with independent state can be created freely.
Override Interfaces
For performance-critical paths, implement these interfaces to bypass reflection:
type Hashable interface {
Hash(hashers map[HashAlgo]Hasher) error
}
type Encryptable interface {
Encrypt(encryptors map[EncryptAlgo]Encryptor) error
}
type Decryptable interface {
Decrypt(encryptors map[EncryptAlgo]Encryptor) error
}
type Maskable interface {
Mask(maskers map[MaskType]Masker) error
}
type Redactable interface {
Redact() error
}
When implemented, the processor calls these methods instead of using reflection.
See Override Interfaces for implementation guidance.
Context Propagation
All operations accept context.Context:
proc.Receive(ctx, obj)
proc.Load(ctx, obj)
proc.Store(ctx, obj)
proc.Send(ctx, obj)
Context flows through to signal emission for tracing integration. Pass your request context to correlate operations with traces.
Thread Safety
Processors are safe for concurrent use. Multiple goroutines can call Receive, Load, Store, and Send simultaneously on the same processor instance.
Configuration methods (SetEncryptor, SetHasher, SetMasker) use internal synchronization and can be called during operation, though typically configuration happens at startup.
proc, _ := cereal.NewProcessor[User]()
proc.SetEncryptor(cereal.EncryptAES, enc)
// Safe: concurrent operations on same processor
go func() { proc.Store(ctx, user1) }()
go func() { proc.Store(ctx, user2) }()
go func() { proc.Load(ctx, user3) }()
Nested Struct Support
Transforms apply to nested structs:
type Address struct {
Street string `json:"street"`
City string `json:"city" send.redact:"[HIDDEN]"`
}
type User struct {
Name string `json:"name" send.mask:"name"`
Address Address `json:"address"` // City will be redacted on Send
}
Both direct fields and nested struct fields are processed.