This guide explains how to use the github.com/bborbe/validation library effectively in Go projects.
- Overview
- Basic Patterns
- Logical Operators
- Common Validators
- Advanced Patterns
- Domain Type Integration
- Error Handling
- Best Practices
- Common Examples
The github.com/bborbe/validation library provides a declarative approach to validation with composable validators. It follows these key principles:
- Composable: Validators can be combined using logical operators
- Named: Field names can be attached to validators for clear error messages
- Type-safe: Works with Go's type system and generics
- Context-aware: All validation functions accept
context.Context
Owner: go-quality-assistant
Applies when: a Go service implements input validation via hand-rolled if/if-else chains in a Validate(ctx context.Context) error method or HTTP handler, instead of using github.com/bborbe/validation (validation.All{...} + validation.Name(...) + validation.NotEmptyString(...) etc.).
Enforcement: rules/go/use-bborbe-validation-not-inline-checks.yml (mechanical first-pass — flags Validate methods containing if-statements that return an errors.* call directly) + judgment-tier LLM adjudication for single-field domain types with one simple check, or packages already importing github.com/bborbe/validation
Why: Hand-rolled validation drifts: every package writes its own "is this empty?" check, error message format, field-naming convention, and short-circuiting policy. Code review can't enforce consistency across 30 services. bborbe/validation provides one composable library with named fields, structured errors, and consistent message formatting — adopting it everywhere eliminates 40+ lines of boilerplate per Validate method and produces consistent error JSON downstream consumers can parse.
import "github.com/bborbe/validation"
func (u User) Validate(ctx context.Context) error {
return validation.All{
validation.Name("username", validation.NotEmptyString(u.Username)),
validation.Name("email", validation.NotEmptyString(u.Email)),
}.Validate(ctx)
}// Leverage existing Validate methods on domain types
func (r Request) Validate(ctx context.Context) error {
return validation.All{
validation.Name("userId", r.UserID), // Uses UserID.Validate()
validation.Name("category", r.Category), // Uses Category.Validate()
validation.Name("priority", r.Priority), // Uses Priority.Validate()
}.Validate(ctx)
}All validators must pass. Use for mandatory field validation:
return validation.All{
validation.Name("username", validation.NotEmptyString(user.Username)),
validation.Name("email", validation.NotEmptyString(user.Email)),
validation.Name("age", validation.HasValidationFunc(func(ctx context.Context) error {
if user.Age < 18 {
return errors.New(ctx, "must be 18 or older")
}
return nil
})),
}.Validate(ctx)At least one validator must pass. Use for optional-but-required scenarios:
// At least one contact method must be provided
return validation.Any{
validation.Name("email", validation.NotEmptyString(r.Email)),
validation.Name("phone", validation.NotEmptyString(r.Phone)),
validation.Name("address", validation.NotEmptyString(r.Address)),
}.Validate(ctx)// Non-empty string
validation.NotEmptyString(value)
// String length constraints
validation.StringMinLength(5)
validation.StringMaxLength(100)
// Regular expression matching
validation.StringRegexp(`^[a-zA-Z0-9]+$`)
// Specific string values
validation.StringEquals("expected")validation.NotNil(pointer)
validation.Nil(pointer)
validation.True(boolValue)
validation.False(boolValue)validation.HasValidationFunc(func(ctx context.Context) error {
if customCondition {
return errors.New(ctx, "custom validation failed")
}
return nil
})return validation.All{
validation.Name("orderType", o.OrderType),
validation.Name("price", validation.HasValidationFunc(func(ctx context.Context) error {
switch o.OrderType {
case "limit":
if o.Price == nil {
return errors.New(ctx, "price required for limit orders")
}
case "market":
if o.Price != nil {
return errors.New(ctx, "price not allowed for market orders")
}
}
return nil
})),
}.Validate(ctx)type Project struct {
Name string
Description string
Owner User
Tasks []Task
}
func (p Project) Validate(ctx context.Context) error {
return validation.All{
validation.Name("name", validation.NotEmptyString(p.Name)),
validation.Name("owner", p.Owner), // Calls User.Validate()
validation.Name("tasks", p.Tasks), // Calls Tasks.Validate()
}.Validate(ctx)
}type Tasks []Task
func (t Tasks) Validate(ctx context.Context) error {
for i, task := range t {
if err := task.Validate(ctx); err != nil {
return errors.Wrapf(ctx, err, "task[%d] validation failed", i)
}
}
return nil
}Well-designed domain types implement their own validation:
// UserType validates against available user types
func (u UserType) Validate(ctx context.Context) error {
validTypes := []UserType{"admin", "user", "guest"}
for _, validType := range validTypes {
if u == validType {
return nil
}
}
return errors.Wrapf(ctx, validation.Error, "userType(%s) is invalid", u)
}
// Status validates against available statuses
func (s Status) Validate(ctx context.Context) error {
validStatuses := []Status{"active", "inactive", "pending"}
for _, validStatus := range validStatuses {
if s == validStatus {
return nil
}
}
return errors.Wrapf(ctx, validation.Error, "status(%s) is invalid", s)
}Always prefer using domain type validation over custom checks:
// GOOD: Uses domain validation
return validation.All{
validation.Name("userType", u.UserType), // Validates against valid user types
validation.Name("status", u.Status), // Validates against valid statuses
}.Validate(ctx)
// AVOID: Custom validation that duplicates domain logic
return validation.All{
validation.Name("userType", validation.NotEmptyString(string(u.UserType))), // Just length check
}.Validate(ctx)Use validation.Name() to provide context in error messages:
return validation.All{
validation.Name("username", validation.NotEmptyString(user.Username)),
validation.Name("email", validation.NotEmptyString(user.Email)),
}.Validate(ctx)
// Error: "username: string is empty"
// Error: "email: string is empty"validation.HasValidationFunc(func(ctx context.Context) error {
if user.Age < 18 {
return errors.New(ctx, "user must be at least 18 years old")
}
return nil
})if err := validator.Validate(ctx); err != nil {
return errors.Wrapf(ctx, err, "validation failed for request")
}// GOOD: Uses domain type's built-in validation
validation.Name("category", u.Category)
// AVOID: Manual validation that might miss business rules
validation.Name("category", validation.NotEmptyString(string(u.Category)))// GOOD: Clear field names
validation.Name("userIdentifier", u.UserIdentifier)
validation.Name("contactMethod", u.ContactMethod)
// AVOID: Generic or unclear names
validation.Name("field1", u.UserIdentifier)
validation.Name("type", u.ContactMethod)return validation.All{
// Required fields
validation.Name("userID", r.UserID),
validation.Name("action", r.Action),
// Permission validation
validation.Name("permissions", r.Permissions),
validation.Name("role", r.Role),
// Conditional validation based on action type
validation.HasValidationFunc(func(ctx context.Context) error {
return r.validateActionSpecificFields(ctx)
}),
}.Validate(ctx)func (r Request) Validate(ctx context.Context) error {
// Simple, declarative validation
return validation.All{
validation.Name("field1", r.Field1),
validation.Name("field2", r.Field2),
validation.Name("complex", validation.HasValidationFunc(r.validateComplexRules)),
}.Validate(ctx)
}
func (r Request) validateComplexRules(ctx context.Context) error {
// Complex logic extracted to separate method
// ... implementation
}// For optional fields that must be valid when present
validation.Any{
validation.StringEquals(""), // Empty is OK
validation.Name("url", validation.StringURL(r.URL)), // OR must be valid URL
}
// For at-least-one-required scenarios
validation.Any{
validation.Name("email", validation.NotEmptyString(r.Email)),
validation.Name("phone", validation.NotEmptyString(r.Phone)),
}func (r SearchRequest) Validate(ctx context.Context) error {
return validation.Any{
validation.Name("name", validation.NotEmptyString(r.Name)),
validation.Name("category", r.Category),
validation.Name("status", r.Status),
validation.Name("userID", r.UserID),
validation.Name("dateRange", r.DateRange),
}.Validate(ctx)
}func (r CreateOrderRequest) Validate(ctx context.Context) error {
return validation.All{
validation.Name("customerID", r.CustomerID),
validation.Name("productID", r.ProductID),
validation.Name("quantity", validation.HasValidationFunc(func(ctx context.Context) error {
if r.Quantity <= 0 {
return errors.Errorf(ctx, "quantity must be greater than 0")
}
return nil
})),
validation.Name("price", validation.HasValidationFunc(func(ctx context.Context) error {
if r.Price != nil && r.Price.Amount <= 0 {
return errors.Errorf(ctx, "price must be greater than 0")
}
return nil
})),
}.Validate(ctx)
}func (p Project) Validate(ctx context.Context) error {
return validation.All{
validation.Name("name", validation.NotEmptyString(p.Name)),
validation.Name("owner", p.Owner),
validation.Name("tasks", p.Tasks),
}.Validate(ctx)
}The validation framework integrates with the project's error handling:
import (
"github.com/bborbe/errors"
"github.com/bborbe/validation"
)
// Domain types use validation.Error for consistent error classification
func (s Status) Validate(ctx context.Context) error {
validStatuses := []Status{"active", "inactive", "pending"}
for _, validStatus := range validStatuses {
if s == validStatus {
return nil
}
}
return errors.Wrapf(ctx, validation.Error, "status(%s) is invalid", s)
}This guide should help you implement consistent, maintainable validation throughout your Go codebase.