Skip to content

zoobz-io/cereal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cereal

CI codecov Go Report Card CodeQL Go Reference License Go Version Release

Boundary-aware serialization for Go. Transform data differently as it crosses system boundaries—encrypt for storage, mask for APIs, hash on receive.

Four Boundaries, One Processor

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)

Install

go get github.com/zoobz-io/cereal

Requires Go 1.24+

Quick Start

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"}
}

Capabilities

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

Why Cereal?

  • 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 genericsProcessor[User] only accepts User
  • 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

Security as Structure

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 masked

Security requirements change in one place. Every serialization path follows.

Documentation

Learn

Guides

  • Encryption — AES, RSA, envelope encryption
  • Masking — PII protection for API responses
  • Providers — JSON, YAML, XML, MessagePack, BSON

Cookbook

Reference

  • API — Complete function documentation
  • Tags — All struct tag options
  • Errors — Error types and handling

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT License — see LICENSE for details.