zoobzio December 26, 2025 Edit this page

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

BoundaryOperationDirectionUse Case
ReceiveReceive()External → AppAPI requests, webhooks, events
LoadLoad()Storage → AppDatabase reads, cache hits
StoreStore()App → StorageDatabase writes, cache sets
SendSend()App → ExternalAPI 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.