gauzer

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 6, 2026 License: MIT Imports: 12 Imported by: 0

README

Gauzer

Struct validation for Go that natively speaks log/slog and OpenTelemetry - so failures land in Datadog, CloudWatch, or Azure Monitor as queryable structured events, not as strings you have to parse.

go get github.com/trycatchkamal/gauzer

Go Reference Go Report Card Build Status


The Problem

Most validation libraries return errors as flat strings. That works fine until you're on-call trying to answer "how often are users failing the age check, and with what values?" in a production system.

What your SREs see today (go-playground/validator):

# In your log aggregator, searching for validation errors:
message: "Key: 'User.Age' Error:Field validation for 'Age' failed on the 'gte' tag"

There is no field, no constraint, no value key. You write a regex parser, or you give up.

What your SREs see with Gauzer:

if err := gauzer.ValidateStruct(ctx, req); err != nil {
    slog.Error("validation failed", "err", err)
    // done.
}
{
  "time": "2024-01-15T10:30:00.000Z",
  "level": "ERROR",
  "msg": "validation failed",
  "err": {
    "field": "Age",
    "constraint": "gte:18",
    "value": "16",
    "type": "int"
  }
}

Every field is a first-class attribute. Your log aggregator can filter on err.field = "Age", group by err.constraint, alert when err.value spikes - no parsing needed.

The DiagnosticEvent Gauzer returns implements both error and slog.LogValuer. Pass it to any structured logger and the nesting happens automatically.


Zero-Friction Migration

If you're already using struct tags for validation, migration is a tag rename:

// Before (go-playground/validator)
type CreateUserRequest struct {
    Username string `validate:"required,min=3,max=50"`
    Age      int    `validate:"gte=18,lte=120"`
    Email    string `validate:"required,email"`
    Role     string `validate:"oneof=admin|user|viewer"`
}

// After (Gauzer) - same constraints, different tag name
type CreateUserRequest struct {
    Username string `gauzer:"required,min=3,max=50"`
    Age      int    `gauzer:"gte=18,lte=120"`
    Email    string `gauzer:"required,email"`
    Role     string `gauzer:"oneof=admin|user|viewer"`
}

Call site:

func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    json.NewDecoder(r.Body).Decode(&req)

    if err := gauzer.ValidateStruct(r.Context(), &req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // ...
}

Gauzer parses and compiles your struct's rules exactly once on first use, then takes the zero-allocation hot path on every subsequent call.


Performance

Benchmarks run on a 2-field struct (Email string, Age int) with all fields passing:

Benchmark ns/op B/op allocs/op
ValidateStruct (happy path) ~61 0 0
IntMinRule (happy path) ~1.58 0 0

How: Struct tags are parsed and dispatched to concrete rule types at registration time. Rule dispatch on the hot path is a simple slice iteration with type-specific assertions. The ~61 ns/op includes strict safety guards (nil-pointer checks, unexported field skipping) to guarantee zero panics in production—a 3 ns trade-off we gladly pay for crash-proof reliability.

On failure: allocations are intentional. Building a DiagnosticEvent payload requires allocating strings. We accept that cost on the sad path to give you a safe, well-formed telemetry payload. The happy path stays at zero.

Run the benchmarks yourself:

go test -bench=. -benchmem ./...

Vendor-Agnostic Telemetry

Gauzer routes validation failures through an Emitter interface:

type Emitter interface {
    Emit(ctx context.Context, event *DiagnosticEvent)
}

The default backend is log/slog - zero configuration required. Add this to your main and you get structured JSON logs immediately:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
OpenTelemetry

To write failures directly onto the active span as attributes:

import "github.com/trycatchkamal/gauzer/otel"

gauzer.SetEmitter(oteladapter.New())

That's the entire integration. The OTel adapter writes attributes under the gauzer.* namespace (gauzer.field, gauzer.constraint, gauzer.value, gauzer.value_type, gauzer.message) on whatever span is active in the context.Context you pass to ValidateStruct. If there is no active span the event is silently dropped - no panics.

AWS CloudWatch and Azure Monitor adapters are on the roadmap. Because the interface is just Emit(ctx, *DiagnosticEvent), you can write your own in about 10 lines.

The core github.com/trycatchkamal/gauzer module has zero external dependencies. The OTel adapter lives in its own go.mod at github.com/trycatchkamal/gauzer/otel and pulls in the OTel SDK only if you opt in.


Why Structured Errors Matter for AI Tooling

As teams integrate AI coding assistants (like Claude, Cursor, or Copilot) into their workflows, the quality of the context you paste into the prompt dictates the quality of the fix you get back.

If you paste a standard validation string into an AI: Error:Field validation for 'Age' failed on the 'gte' tag

The AI has to guess: What is the struct name? What is the exact constraint? What was the actual input? It might suggest a fix, but it will often hallucinate the wrong field or rule.

Because Gauzer returns a strict DiagnosticEvent, you aren't pasting an error message, you are pasting machine-readable schema metadata:

{  "field": "Age",  "constraint": "gte:18",  "value": "16",  "type": "int"}

When you hand this payload to an AI agent, it has zero ambiguity. It knows the exact field, the exact rule, and the exact failing value. It doesn't just suggest a fix, it can write the exact struct tag modification or input validation logic required to resolve it on the first attempt.

For SREs building internal AIOps workflows, this same structure means you can reliably feed validation failures into LLMs for root-cause analysis without writing custom regex parsers to clean up log lines first.

PII & Security

By default, Gauzer logs the failing value to help SREs debug edge cases. However, for fields containing PII (emails, SSNs, API keys), you can use the mask modifier.

The field will still be validated against your rules, but if it fails, the raw value will be replaced with *** in your logs and OTel spans.

type User struct {
    Email    string `gauzer:"required,email,mask"` // Failing value becomes "***"
    Password string `gauzer:"required,min=8,mask"` // Failing value becomes "***"
    Age      int    `gauzer:"gte=18"`              // Failing value remains "16"
}

Supported Tags (v0.1.0)

Presence
Tag Description
required Non-zero value required. For strings, rejects empty and whitespace-only.
omitempty Skip all rules for this field if it is the zero value.
Ordering & Comparison
Tag Description
min=N Type-aware minimum: string length, slice/map element count, or numeric value.
max=N Type-aware maximum.
gte=N Value >= N.
lte=N Value <= N.
gt=N Value > N.
lt=N Value < N.
eq=N Value == N.
ne=N Value != N.
len=N Exact length (string rune count or collection element count).
Format
Tag Description
email Valid email address (structural check, no network lookup).
url Valid URL with scheme and host.
uri Valid URI with scheme (host optional).
uuid Canonical UUID format (8-4-4-4-12 hex).
ip Valid IPv4 or IPv6 address.
regexp=<pattern> String matches the given regular expression. Pattern is compiled once at startup.
String Content
Tag Description
oneof=a|b|c Value must be one of the pipe-separated options.
contains=<s> String contains substring.
excludes=<s> String does not contain substring.
startswith=<s> String has the given prefix.
endswith=<s> String has the given suffix.
Collections
Tag Description
unique Slice contains no duplicate elements.
dive Apply the rules that follow to each element of the slice or array. Example: gauzer:"min=1,dive,min=3" - slice must have at least 1 element, and each element (string) must be at least 3 characters.
Cross-Field
Tag Description
eqfield=OtherField Value must equal the named sibling field.
nefield=OtherField Value must not equal the named sibling field.

Roadmap

Check out the ROADMAP to see what's planned for v0.2.0, including OTel metrics, nested dive, and enterprise dependency injection.

Contributing

Bug reports and pull requests are welcome. For significant changes, open an issue first to discuss the approach.

go test ./...
go test -bench=. -benchmem ./...

Supporting This Work

If Gauzer saves you from writing custom validation loggers, starring the repo helps other Go developers discover it.

Sponsors

If Gauzer saves your team time, consider sponsoring its development.

License

MIT. See LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ResetEmitter

func ResetEmitter()

ResetEmitter restores the default SlogEmitter. Useful in tests to avoid cross-test pollution.

func SetEmitter

func SetEmitter(e Emitter)

SetEmitter replaces the active telemetry backend. Thread-safe; safe to call from multiple goroutines.

func ValidateStruct

func ValidateStruct(ctx context.Context, obj any) error

ValidateStruct validates a struct using `gauzer` struct tags. Reflection is used ONLY during first-time setup (cached thereafter). The Emitter is pulled once and injected down into pure functions.

Types

type CollectionLenRule

type CollectionLenRule struct {
	Field string
	Len   int
}

CollectionLenRule validates that a collection has exactly Len elements.

func (CollectionLenRule) Validate

func (r CollectionLenRule) Validate(value any, _ any) *DiagnosticEvent

type CollectionMaxLenRule

type CollectionMaxLenRule struct {
	Field string
	Max   int
}

CollectionMaxLenRule validates that a collection has at most Max elements.

func (CollectionMaxLenRule) Validate

func (r CollectionMaxLenRule) Validate(value any, _ any) *DiagnosticEvent

type CollectionMinLenRule

type CollectionMinLenRule struct {
	Field string
	Min   int
}

CollectionMinLenRule validates that a collection has at least Min elements.

func (CollectionMinLenRule) Validate

func (r CollectionMinLenRule) Validate(value any, _ any) *DiagnosticEvent

type ContainsRule

type ContainsRule struct {
	Field  string
	Substr string
}

ContainsRule validates that a string contains Substr.

func (ContainsRule) Validate

func (r ContainsRule) Validate(value any, _ any) *DiagnosticEvent

type DefaultSlogEmitter

type DefaultSlogEmitter struct{}

DefaultSlogEmitter is the zero-config fallback. It is intentionally a no-op: the *DiagnosticEvent returned by ValidateStruct implements slog.LogValuer, so callers log it themselves without double-logging.

func (DefaultSlogEmitter) Emit

type DiagnosticEvent

type DiagnosticEvent struct {
	Field      string
	Constraint string
	Value      string // stringified & truncated by the Rule, max 64 chars
	ValueType  string // e.g. "string", "int", "unknown"
	Message    string
}

DiagnosticEvent is the structured payload emitted on every validation failure. Value is always a stringified, truncated copy of the failing input — never `any` — to prevent PII leaks and log-bloat in cloud billing.

func (DiagnosticEvent) Error

func (e DiagnosticEvent) Error() string

Error implements the standard error interface.

func (DiagnosticEvent) LogValue

func (e DiagnosticEvent) LogValue() slog.Value

LogValue implements slog.LogValuer so the event nests under "err" in JSON output.

type DiveRule

type DiveRule struct {
	Field    string
	SubRules []Rule
}

DiveRule iterates over a slice and runs SubRules against every element. On first failure it returns a DiagnosticEvent with Field set to "Name[i]".

func (DiveRule) Validate

func (r DiveRule) Validate(value any, _ any) *DiagnosticEvent

type EmailRule

type EmailRule struct {
	Field string
}

EmailRule validates that a string value looks like an email address. Uses a minimal structural check (no reflection, no regex package at call-time).

func (EmailRule) Validate

func (r EmailRule) Validate(value any, _ any) *DiagnosticEvent

type Emitter

type Emitter interface {
	Emit(ctx context.Context, event *DiagnosticEvent)
}

Emitter routes DiagnosticEvents to any telemetry backend.

type EndsWithRule

type EndsWithRule struct {
	Field  string
	Suffix string
}

EndsWithRule validates that a string has the given suffix.

func (EndsWithRule) Validate

func (r EndsWithRule) Validate(value any, _ any) *DiagnosticEvent

type EqFieldRule

type EqFieldRule struct {
	Field      string
	OtherField string
}

EqFieldRule validates that this field's value equals the named sibling field. parent must be the containing struct value passed from ValidateStruct.

func (EqFieldRule) Validate

func (r EqFieldRule) Validate(value any, parent any) *DiagnosticEvent

type EqRule

type EqRule struct {
	Field     string
	Threshold float64
}

EqRule validates value == Threshold (type-aware; strings/slices compare length).

func (EqRule) Validate

func (r EqRule) Validate(value any, _ any) *DiagnosticEvent

type ExcludesRule

type ExcludesRule struct {
	Field  string
	Substr string
}

ExcludesRule validates that a string does NOT contain Substr.

func (ExcludesRule) Validate

func (r ExcludesRule) Validate(value any, _ any) *DiagnosticEvent

type FloatMaxRule

type FloatMaxRule struct {
	Field string
	Max   float64
}

FloatMaxRule validates that a float64 value is <= Max.

func (FloatMaxRule) Validate

func (r FloatMaxRule) Validate(value any, _ any) *DiagnosticEvent

type FloatMinRule

type FloatMinRule struct {
	Field string
	Min   float64
}

FloatMinRule validates that a float64 value is >= Min.

func (FloatMinRule) Validate

func (r FloatMinRule) Validate(value any, _ any) *DiagnosticEvent

type GtRule

type GtRule struct {
	Field     string
	Threshold float64
}

GtRule validates value > Threshold (type-aware).

func (GtRule) Validate

func (r GtRule) Validate(value any, _ any) *DiagnosticEvent

type GteRule

type GteRule struct {
	Field     string
	Threshold float64
}

GteRule validates value >= Threshold (type-aware).

func (GteRule) Validate

func (r GteRule) Validate(value any, _ any) *DiagnosticEvent

type IPRule

type IPRule struct {
	Field string
}

IPRule validates that a string is a valid IPv4 or IPv6 address. Uses net.ParseIP — stdlib, no reflection.

func (IPRule) Validate

func (r IPRule) Validate(value any, _ any) *DiagnosticEvent

type IntMaxRule

type IntMaxRule struct {
	Field string
	Max   int
}

IntMaxRule validates that an int value is <= Max.

func (IntMaxRule) Validate

func (r IntMaxRule) Validate(value any, _ any) *DiagnosticEvent

type IntMinRule

type IntMinRule struct {
	Field string
	Min   int
}

IntMinRule validates that an int value is >= Min. Pattern: strict type assertion, strconv for conversion, string concatenation for messages.

func (IntMinRule) Validate

func (r IntMinRule) Validate(value any, _ any) *DiagnosticEvent

type LtRule

type LtRule struct {
	Field     string
	Threshold float64
}

LtRule validates value < Threshold (type-aware).

func (LtRule) Validate

func (r LtRule) Validate(value any, _ any) *DiagnosticEvent

type LteRule

type LteRule struct {
	Field     string
	Threshold float64
}

LteRule validates value <= Threshold (type-aware).

func (LteRule) Validate

func (r LteRule) Validate(value any, _ any) *DiagnosticEvent

type NeFieldRule

type NeFieldRule struct {
	Field      string
	OtherField string
}

NeFieldRule validates that this field's value does NOT equal the named sibling field. parent must be the containing struct value passed from ValidateStruct.

func (NeFieldRule) Validate

func (r NeFieldRule) Validate(value any, parent any) *DiagnosticEvent

type NeRule

type NeRule struct {
	Field     string
	Threshold float64
}

NeRule validates value != Threshold (type-aware).

func (NeRule) Validate

func (r NeRule) Validate(value any, _ any) *DiagnosticEvent

type OneOfRule

type OneOfRule struct {
	Field   string
	Allowed []string
}

OneOfRule validates that a string value is one of the allowed values.

func (OneOfRule) Validate

func (r OneOfRule) Validate(value any, _ any) *DiagnosticEvent

type RegexRule

type RegexRule struct {
	Field   string
	Pattern string
	// contains filtered or unexported fields
}

RegexRule validates that a string matches the compiled regexp. The regexp is compiled at construction time (NewRegexRule), never during Validate.

func NewRegexRule

func NewRegexRule(field, pattern string) (RegexRule, error)

NewRegexRule compiles the pattern once and returns a ready-to-use RegexRule. Returns an error if the pattern is invalid instead of panicking.

func (RegexRule) Validate

func (r RegexRule) Validate(value any, _ any) *DiagnosticEvent

type RequiredRule

type RequiredRule struct {
	Field string
}

RequiredRule validates that a string value is non-empty and not whitespace-only.

func (RequiredRule) Validate

func (r RequiredRule) Validate(value any, _ any) *DiagnosticEvent

type Rule

type Rule interface {
	Validate(value any, parent any) *DiagnosticEvent
}

Rule is implemented by every validation constraint. Validate must NOT use reflection inside simple rules; use strict type assertions instead. On success it returns nil (zero-alloc happy path). parent is the containing struct value — nil unless the rule requires cross-field access (e.g. eqfield).

type StartsWithRule

type StartsWithRule struct {
	Field  string
	Prefix string
}

StartsWithRule validates that a string has the given prefix.

func (StartsWithRule) Validate

func (r StartsWithRule) Validate(value any, _ any) *DiagnosticEvent

type StringLenRule

type StringLenRule struct {
	Field string
	Len   int
}

StringLenRule validates that a string has exactly Len runes.

func (StringLenRule) Validate

func (r StringLenRule) Validate(value any, _ any) *DiagnosticEvent

type StringMaxLengthRule

type StringMaxLengthRule struct {
	Field string
	Max   int
}

StringMaxLengthRule validates that a string's rune length is <= Max.

func (StringMaxLengthRule) Validate

func (r StringMaxLengthRule) Validate(value any, _ any) *DiagnosticEvent

type StringMinLengthRule

type StringMinLengthRule struct {
	Field string
	Min   int
}

StringMinLengthRule validates that a string's rune length is >= Min.

func (StringMinLengthRule) Validate

func (r StringMinLengthRule) Validate(value any, _ any) *DiagnosticEvent

type StringRequiredRule

type StringRequiredRule struct {
	Field string
}

StringRequiredRule validates that a string is non-empty and not whitespace-only.

func (StringRequiredRule) Validate

func (r StringRequiredRule) Validate(value any, _ any) *DiagnosticEvent

type URIRule

type URIRule struct {
	Field string
}

URIRule validates that a string is a valid URI (scheme required; host optional).

func (URIRule) Validate

func (r URIRule) Validate(value any, _ any) *DiagnosticEvent

type URLRule

type URLRule struct {
	Field string
}

URLRule validates that a string is a fully-qualified URL (scheme + host required).

func (URLRule) Validate

func (r URLRule) Validate(value any, _ any) *DiagnosticEvent

type UUIDRule

type UUIDRule struct {
	Field string
}

UUIDRule validates that a string is a canonical UUID v4 format. Zero-alloc: checks length (36) and hyphen positions only — no regex. Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12 hex digits)

func (UUIDRule) Validate

func (r UUIDRule) Validate(value any, _ any) *DiagnosticEvent

type UniqueRule

type UniqueRule struct {
	Field string
}

UniqueRule validates that a slice contains no duplicate values.

func (UniqueRule) Validate

func (r UniqueRule) Validate(value any, _ any) *DiagnosticEvent

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL