Overview
Data crosses boundaries constantly. Each crossing demands different treatment.
Cereal provides boundary-aware serialization: transform data differently depending on where it's going or where it came from.
type User struct {
ID string `json:"id"`
Email string `json:"email" store.encrypt:"aes" load.decrypt:"aes" send.mask:"email"`
Password string `json:"password" receive.hash:"argon2"`
SSN string `json:"ssn" send.mask:"ssn"`
Token string `json:"token" send.redact:"[REDACTED]"`
}
func (u User) Clone() User { return u }
func main() {
ctx := context.Background()
proc, _ := cereal.NewProcessor[User]()
enc, _ := cereal.AES(key)
proc.SetEncryptor(cereal.EncryptAES, enc)
// Receive: hash password from incoming request
user, _ := proc.Receive(ctx, incomingUser)
// Store: encrypt email before saving to database
stored, _ := proc.Store(ctx, user)
// Load: decrypt email when reading from database
loaded, _ := proc.Load(ctx, stored)
// Send: mask email and SSN, redact token for API response
sanitized, _ := proc.Send(ctx, loaded)
}
Four boundaries. Four operations. Tags declare which transforms apply where.
The Four Boundaries
| Boundary | Operation | Direction | Use Case |
|---|---|---|---|
| Receive | Receive() | External → App | API requests, webhooks, events |
| Load | Load() | Storage → App | Database reads, cache hits |
| Store | Store() | App → Storage | Database writes, cache sets |
| Send | Send() | App → External | API responses, outbound events |
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Codec │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Processor[T] │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ Transforms │ │ Codec │ │ Sentinel │ │ │
│ │ │ │ │ (optional) │ │ Metadata │ │ │
│ │ │ Encryptors │ │ Marshal() │ │ (cached) │ │ │
│ │ │ Hashers │ │ Unmarshal() │ │ │ │ │
│ │ │ Maskers │ │ │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Providers │ │
│ │ JSON XML YAML MessagePack BSON │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The Processor[T] provides boundary-aware transforms. A codec may optionally be set for byte-level serialization via the secondary API.
Capabilities
Encryption - AES-GCM, RSA-OAEP, envelope encryption. Applied on store, reversed on load.
Hashing - SHA-256, SHA-512, Argon2, bcrypt. Applied on receive for incoming data.
Masking - Content-aware partial masking. SSN becomes ***-**-6789. Email becomes a***@example.com. Eight built-in patterns.
Redaction - Full replacement. send.redact:"[HIDDEN]" replaces the entire value.
Override Interfaces - Implement Encryptable, Hashable, Maskable, or Redactable to bypass reflection.
Design Principles
Boundary Awareness
Different boundaries need different transforms:
type Secret struct {
// Encrypt when storing, decrypt when loading
Value string `store.encrypt:"aes" load.decrypt:"aes"`
// Only mask when sending externally
Key string `send.mask:"card"`
}
Type Safety
Processors are generic. A Processor[User] only accepts User values.
proc, _ := cereal.NewProcessor[User]()
proc.Store(ctx, user) // OK
proc.Store(ctx, otherType) // Compile error
Non-Destructive
Original values are never modified. The processor clones before transforming.
original := User{Email: "alice@example.com"}
sanitized, _ := proc.Send(ctx, original)
// original.Email is still "alice@example.com"
// sanitized contains the masked value
Provider Agnostic
Swap codecs without changing your types.
proc, _ := cereal.NewProcessor[User]()
proc.SetCodec(json.New())
proc.SetCodec(yaml.New())
proc.SetCodec(bson.New())
// Same type, same tags, different wire format
Observable
All operations emit signals via capitan for metrics and tracing integration.