Boundary-aware serialization for Go. Transform data differently as it crosses system boundaries—encrypt for storage, mask for APIs, hash on receive.
Data crosses boundaries constantly. Each crossing demands different treatment:
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]"`
}- Receive — Data arriving from external sources. Hash passwords, normalize inputs.
- Load — Data coming from storage. Decrypt sensitive fields.
- Store — Data going to storage. Encrypt before persisting.
- Send — Data going to external destinations. Mask PII, redact secrets.
The struct declares intent. The processor handles the rest:
proc, _ := cereal.NewProcessor[User]()
proc.SetEncryptor(cereal.EncryptAES, encryptor)
// Receive: hash password
received, _ := proc.Receive(ctx, user)
// Store: encrypt email
stored, _ := proc.Store(ctx, received)
// Load: decrypt email
loaded, _ := proc.Load(ctx, stored)
// Send: mask email, redact token
sent, _ := proc.Send(ctx, loaded)go get github.com/zoobz-io/cerealRequires Go 1.24+
package main
import (
"context"
"fmt"
"github.com/zoobz-io/cereal"
"github.com/zoobz-io/cereal/json"
)
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"`
}
func (u User) Clone() User { return u }
func main() {
ctx := context.Background()
// Create processor
proc, _ := cereal.NewProcessor[User]()
// Configure encryption
enc, _ := cereal.AES([]byte("32-byte-key-for-aes-256-encrypt!"))
proc.SetEncryptor(cereal.EncryptAES, enc)
user := User{
ID: "123",
Email: "alice@example.com",
Password: "secret",
}
// Store: encrypts email before persisting
stored, _ := proc.Store(ctx, user)
fmt.Println(stored.Email)
// <encrypted>
// Load: decrypts email from storage
loaded, _ := proc.Load(ctx, stored)
fmt.Println(loaded.Email)
// alice@example.com
// Send: masks email for API response
sent, _ := proc.Send(ctx, user)
fmt.Println(sent.Email)
// a***@example.com
// Optional: codec-aware API for marshaling
proc.SetCodec(json.New())
sentBytes, _ := proc.Encode(ctx, &user)
fmt.Println(string(sentBytes))
// {"id":"123","email":"a***@example.com","password":"secret"}
}| Capability | Boundaries | Description | Docs |
|---|---|---|---|
| Encryption | store/load | AES-GCM, RSA-OAEP, envelope | Guide |
| Masking | send | Email, SSN, phone, card, IP, UUID, IBAN, name | Guide |
| Hashing | receive | SHA-256, SHA-512, Argon2, bcrypt | Reference |
| Redaction | send | Full replacement with custom string | Reference |
- Boundary-specific transforms — Different rules for storage vs. API responses vs. incoming data
- Declarative via struct tags — Security requirements live with the type definition
- Non-destructive — Original values never modified; processor clones before transforming
- Type-safe generics —
Processor[User]only acceptsUser - Thread-safe — Processors safe for concurrent use across goroutines
- Provider agnostic — JSON, YAML, XML, MessagePack, BSON via optional
SetCodec - Observable — Emits signals for metrics and tracing via capitan
Cereal enables a pattern: declare sensitivity once, enforce everywhere.
Data sensitivity lives in the type definition, not scattered across handlers. When a field is marked for encryption or masking, every boundary crossing respects that declaration automatically. Business logic remains unaware of security transforms—it works with plain structs while the processor handles the rest.
// The type declares intent
type Payment struct {
ID string `json:"id"`
Card string `json:"card" store.encrypt:"aes" send.mask:"card"`
Amount int `json:"amount"`
}
// Business logic stays clean
func ProcessPayment(p *Payment) error {
// No encryption calls, no masking logic
// Just domain operations on plain fields
return chargeCard(p.Card, p.Amount)
}
// Boundaries handle transforms
stored, _ := proc.Store(ctx, payment) // Card encrypted
sent, _ := proc.Send(ctx, payment) // Card maskedSecurity requirements change in one place. Every serialization path follows.
- Overview — Design philosophy
- Quick Start — Get started in minutes
- Concepts — Boundaries, processors, transforms
- Architecture — Internal design and components
- Encryption — AES, RSA, envelope encryption
- Masking — PII protection for API responses
- Providers — JSON, YAML, XML, MessagePack, BSON
- Escape Hatches — Custom transforms and overrides
- Key Rotation — Zero-downtime encryption key updates
- Code Generation — Generating processors from schemas
- API — Complete function documentation
- Tags — All struct tag options
- Errors — Error types and handling
See CONTRIBUTING.md for guidelines.
MIT License — see LICENSE for details.