zoobzio December 26, 2025 Edit this page

Code Generation

Override interfaces bypass reflection but require manual implementation. Code generation automates this, keeping implementations in sync with struct tags.

The Problem

Manual override implementations drift from struct definitions:

type User struct {
    Email    string `json:"email" store.encrypt:"aes" load.decrypt:"aes"`
    Password string `json:"password" receive.hash:"argon2"`
    SSN      string `json:"ssn" send.mask:"ssn"`
    // Add new field...
    Phone    string `json:"phone" send.mask:"phone"`
}

// Oops - forgot to update Mask() for Phone field
func (u *User) Mask(maskers map[cereal.MaskType]cereal.Masker) error {
    u.Email = maskers[cereal.MaskEmail].Mask(u.Email)
    u.SSN = maskers[cereal.MaskSSN].Mask(u.SSN)
    // Phone not masked!
    return nil
}

Code generation solves this by reading struct tags and generating implementations automatically.

Simple Generator

A minimal generator using go/ast and go/parser:

//go:build ignore

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
    "io"
    "os"
    "reflect"
    "strings"
    "text/template"
)

func main() {
    if len(os.Args) < 3 {
        fmt.Fprintln(os.Stderr, "usage: go run gen.go <file.go> <TypeName>")
        os.Exit(1)
    }

    filename := os.Args[1]
    typeName := os.Args[2]

    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    spec := findType(f, typeName)
    if spec == nil {
        fmt.Fprintf(os.Stderr, "type %s not found\n", typeName)
        os.Exit(1)
    }

    fields := extractFields(spec)
    if err := generate(os.Stdout, typeName, fields); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

type Field struct {
    Name       string
    EncryptAlg string // store.encrypt value
    DecryptAlg string // load.decrypt value
    HashAlg    string // receive.hash value
    MaskType   string // send.mask value
    RedactVal  string // send.redact value
}

func findType(f *ast.File, name string) *ast.TypeSpec {
    for _, decl := range f.Decls {
        gen, ok := decl.(*ast.GenDecl)
        if !ok || gen.Tok != token.TYPE {
            continue
        }
        for _, spec := range gen.Specs {
            ts := spec.(*ast.TypeSpec)
            if ts.Name.Name == name {
                return ts
            }
        }
    }
    return nil
}

func extractFields(spec *ast.TypeSpec) []Field {
    st, ok := spec.Type.(*ast.StructType)
    if !ok {
        return nil
    }

    var fields []Field
    for _, f := range st.Fields.List {
        if len(f.Names) == 0 || f.Tag == nil {
            continue
        }

        tag := reflect.StructTag(strings.Trim(f.Tag.Value, "`"))
        field := Field{Name: f.Names[0].Name}

        if v, ok := tag.Lookup("store.encrypt"); ok {
            field.EncryptAlg = v
        }
        if v, ok := tag.Lookup("load.decrypt"); ok {
            field.DecryptAlg = v
        }
        if v, ok := tag.Lookup("receive.hash"); ok {
            field.HashAlg = v
        }
        if v, ok := tag.Lookup("send.mask"); ok {
            field.MaskType = v
        }
        if v, ok := tag.Lookup("send.redact"); ok {
            field.RedactVal = v
        }

        if field.EncryptAlg != "" || field.DecryptAlg != "" ||
            field.HashAlg != "" || field.MaskType != "" || field.RedactVal != "" {
            fields = append(fields, field)
        }
    }
    return fields
}

func generate(w io.Writer, typeName string, fields []Field) error {
    return tmpl.Execute(w, map[string]any{
        "Type":   typeName,
        "Fields": fields,
    })
}

var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
    "hasEncrypt": func(fields []Field) bool {
        for _, f := range fields {
            if f.EncryptAlg != "" {
                return true
            }
        }
        return false
    },
    "hasDecrypt": func(fields []Field) bool {
        for _, f := range fields {
            if f.DecryptAlg != "" {
                return true
            }
        }
        return false
    },
    "hasHash": func(fields []Field) bool {
        for _, f := range fields {
            if f.HashAlg != "" {
                return true
            }
        }
        return false
    },
    "hasMask": func(fields []Field) bool {
        for _, f := range fields {
            if f.MaskType != "" {
                return true
            }
        }
        return false
    },
    "hasRedact": func(fields []Field) bool {
        for _, f := range fields {
            if f.RedactVal != "" {
                return true
            }
        }
        return false
    },
}).Parse(`
// Code generated by gen.go. DO NOT EDIT.

package main

import (
    "encoding/base64"
    "github.com/zoobzio/cereal"
)

{{- $type := .Type }}

{{ if hasEncrypt .Fields -}}
func (x *{{ $type }}) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
{{- range .Fields }}
{{- if .EncryptAlg }}
    if x.{{ .Name }} != "" {
        enc := encryptors[cereal.EncryptAlgo("{{ .EncryptAlg }}")]
        ciphertext, err := enc.Encrypt([]byte(x.{{ .Name }}))
        if err != nil {
            return err
        }
        x.{{ .Name }} = base64.StdEncoding.EncodeToString(ciphertext)
    }
{{- end }}
{{- end }}
    return nil
}
{{ end }}

{{ if hasDecrypt .Fields -}}
func (x *{{ $type }}) Decrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
{{- range .Fields }}
{{- if .DecryptAlg }}
    if x.{{ .Name }} != "" {
        enc := encryptors[cereal.EncryptAlgo("{{ .DecryptAlg }}")]
        ciphertext, err := base64.StdEncoding.DecodeString(x.{{ .Name }})
        if err != nil {
            return err
        }
        plaintext, err := enc.Decrypt(ciphertext)
        if err != nil {
            return err
        }
        x.{{ .Name }} = string(plaintext)
    }
{{- end }}
{{- end }}
    return nil
}
{{ end }}

{{ if hasHash .Fields -}}
func (x *{{ $type }}) Hash(hashers map[cereal.HashAlgo]cereal.Hasher) error {
{{- range .Fields }}
{{- if .HashAlg }}
    if x.{{ .Name }} != "" {
        hasher := hashers[cereal.HashAlgo("{{ .HashAlg }}")]
        hashed, err := hasher.Hash([]byte(x.{{ .Name }}))
        if err != nil {
            return err
        }
        x.{{ .Name }} = hashed
    }
{{- end }}
{{- end }}
    return nil
}
{{ end }}

{{ if hasMask .Fields -}}
func (x *{{ $type }}) Mask(maskers map[cereal.MaskType]cereal.Masker) error {
{{- range .Fields }}
{{- if .MaskType }}
    if x.{{ .Name }} != "" {
        x.{{ .Name }} = maskers[cereal.MaskType("{{ .MaskType }}")].Mask(x.{{ .Name }})
    }
{{- end }}
{{- end }}
    return nil
}
{{ end }}

{{ if hasRedact .Fields -}}
func (x *{{ $type }}) Redact() error {
{{- range .Fields }}
{{- if .RedactVal }}
    x.{{ .Name }} = "{{ .RedactVal }}"
{{- end }}
{{- end }}
    return nil
}
{{ end }}
`))

Usage

Add a go:generate directive to your type file:

//go:generate go run gen.go user.go User

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 }

Run generation:

go generate ./...

Output (user_cereal.go):

// Code generated by gen.go. DO NOT EDIT.

package main

import (
    "encoding/base64"
    "github.com/zoobzio/cereal"
)

func (x *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    if x.Email != "" {
        enc := encryptors[cereal.EncryptAlgo("aes")]
        ciphertext, err := enc.Encrypt([]byte(x.Email))
        if err != nil {
            return err
        }
        x.Email = base64.StdEncoding.EncodeToString(ciphertext)
    }
    return nil
}

func (x *User) Decrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    if x.Email != "" {
        enc := encryptors[cereal.EncryptAlgo("aes")]
        ciphertext, err := base64.StdEncoding.DecodeString(x.Email)
        if err != nil {
            return err
        }
        plaintext, err := enc.Decrypt(ciphertext)
        if err != nil {
            return err
        }
        x.Email = string(plaintext)
    }
    return nil
}

func (x *User) Hash(hashers map[cereal.HashAlgo]cereal.Hasher) error {
    if x.Password != "" {
        hasher := hashers[cereal.HashAlgo("argon2")]
        hashed, err := hasher.Hash([]byte(x.Password))
        if err != nil {
            return err
        }
        x.Password = hashed
    }
    return nil
}

func (x *User) Mask(maskers map[cereal.MaskType]cereal.Masker) error {
    if x.Email != "" {
        x.Email = maskers[cereal.MaskType("email")].Mask(x.Email)
    }
    if x.SSN != "" {
        x.SSN = maskers[cereal.MaskType("ssn")].Mask(x.SSN)
    }
    return nil
}

func (x *User) Redact() error {
    x.Token = "[REDACTED]"
    return nil
}

Integration with CI

Add generation check to CI:

- name: Check generated code
  run: |
    go generate ./...
    git diff --exit-code || (echo "Generated code out of date" && exit 1)

This fails the build if someone modifies struct tags without regenerating.

Extending the Generator

The basic generator handles string fields. Extensions to consider:

Nested structs:

type Address struct {
    City string `json:"city" send.redact:"[HIDDEN]"`
}

type User struct {
    Address Address `json:"address"`
}

Recursively process embedded/nested types.

Slice and map fields:

type User struct {
    Emails []string `json:"emails" send.mask:"email"`
}

Generate loops over elements.

Conditional transforms:

type User struct {
    Email string `json:"email" store.encrypt:"aes,if=Sensitive"`
}

Parse tag options and generate conditionals.

byte fields:

type Record struct {
    Data []byte `json:"data" store.encrypt:"aes"`
}

Skip base64 encoding for byte slices.

Alternative: Reflection at Init

If code generation feels heavy, cache reflection work at init time:

var userFields struct {
    email    reflect.StructField
    password reflect.StructField
}

func init() {
    t := reflect.TypeOf(User{})
    userFields.email, _ = t.FieldByName("Email")
    userFields.password, _ = t.FieldByName("Password")
}

func (u *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
    // Use cached field info
    // Still faster than full reflection each call
}

This trades startup cost for runtime performance without full code generation.

When to Generate

Use code generation when:

  1. Many types — Manual implementations don't scale
  2. Frequent changes — Tags change often, implementations drift
  3. Strict correctness — Can't afford missed fields
  4. Team scale — Multiple developers modifying types

Skip code generation when:

  1. Few types — Manual is manageable
  2. Stable schema — Types rarely change
  3. Custom logic — Transforms need runtime decisions
  4. Simple needs — Reflection overhead acceptable