validator

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2025 License: BSD-3-Clause Imports: 6 Imported by: 0

README

Go Validator

A flexible validation library for Go, supporting composable validation, error aggregation, and structured error reporting.

Features

  • Validate slices and nested structs with custom logic
  • Required/Optional value checks
  • Structured error reporting for fields and slices
  • Composable error handling
  • Table-driven scenario testing

Installation

Add to your Go module:

go get github.com/alextanhongpin/errors/validator

Usage

Required and Optional
err := validator.Required(nil) // returns ErrRequired
err := validator.Required("foo") // returns nil
err := validator.Optional(nil, errors.New("fail")) // returns nil
err := validator.Optional("foo", errors.New("fail")) // returns "fail"
Handling Slices and Nested Validators
Slices

To validate each item in a slice, use ValidateMany:

type Item struct {
  Value string
}
func (i Item) Validate() error {
  return validator.Required(i.Value, validator.Assert(len(i.Value) >= 3, "value must be at least 3 characters"))
}
items := []Item{{"foo"}, {""}, {"bar"}}
err := validator.ValidateMany(items)
if err != nil {
  fmt.Println(err) // errorSlice with index keys for failed items
}

You can also require the slice itself to be non-empty:

err := validator.Required(items, validator.ValidateMany(items))
Nested Validators

For nested structs, call their Validate method inside your parent struct's validation:

type Child struct {
  Name string
}
func (c Child) Validate() error {
  return validator.Required(c.Name)
}
type Parent struct {
  Name   string
  Childs []Child
}
func (p Parent) Validate() error {
  return validator.Map(map[string]error{
    "name": validator.Required(p.Name),
    "childs": validator.Required(p.Childs, validator.ValidateMany(p.Childs)),
  })
}

This pattern works for any level of nesting, and errors will be aggregated and mapped to their respective fields and indices.

Validating Nested Structs

To validate nested structs, simply call their Validate method inside the parent struct's validation logic. Errors will be mapped to the corresponding field name and can be nested as deeply as needed.

type Address struct {
  Street string
  City   string
}
func (a Address) Validate() error {
  return validator.Map(map[string]error{
    "street": validator.Required(a.Street),
    "city":   validator.Required(a.City),
  })
}

type User struct {
  Name    string
  Address Address
}
func (u User) Validate() error {
  return validator.Map(map[string]error{
    "name":    validator.Required(u.Name),
    "address": u.Address.Validate(), // Nested validation
  })
}

user := User{Name: "", Address: Address{Street: "", City: "NYC"}}
err := user.Validate()
if err != nil {
  b, _ := json.MarshalIndent(err, "", "  ")
  fmt.Println(string(b))
  // Output:
  // {
  //   "address": {
  //     "street": "required"
  //   },
  //   "name": "required"
  // }
}

This approach works for any level of nesting, and errors will be aggregated and mapped to their respective fields.

Real-world API validation example
// Define your types
 type Product struct {
   Code  string
   Price float64
 }
 func (p Product) Validate() error {
   return validator.Map(map[string]error{
     "code":  validator.Required(p.Code, validator.Assert(len(p.Code) >= 3, "code must be at least 3 characters")),
     "price": validator.Assert(p.Price >= 1, "price must be at least 1"),
   })
 }
 type Order struct {
   ID     string
   Amount float64
   Product Product
 }
 func (o Order) Validate() error {
   return validator.Map(map[string]error{
     "id":     validator.Required(o.ID, validator.Assert(len(o.ID) >= 4, "order id must be at least 4 characters")),
     "amount": validator.Required(o.Amount, validator.Assert(o.Amount >= 1, "amount must be at least 1")),
     "product": o.Product.Validate(),
   })
 }
 type Address struct {
   Street string
   City   string
   Zip    string
 }
 func (a Address) Validate() error {
   return validator.Map(map[string]error{
     "street": validator.Required(a.Street, validator.Assert(len(a.Street) >= 5, "street must be at least 5 characters")),
     "city":   validator.Required(a.City),
     "zip":    validator.Required(a.Zip, validator.Assert(len(a.Zip) == 5, "zip must be 5 characters")),
   })
 }
 type User struct {
   Name     string
   Email    string
   Address  Address
   Orders   []Order
   Password string
 }
 func (u User) Validate() error {
   passwordConds := map[string]bool{
     "must be at least 8 characters": len(u.Password) >= 8,
     "must contain a number":         containsNumber(u.Password),
     "must contain a capital letter": containsCapital(u.Password),
   }
   return validator.Map(map[string]error{
     "name":     validator.Required(u.Name, validator.Assert(len(u.Name) >= 3, "name must be at least 3 characters")),
     "email":    validator.Required(u.Email, validator.Assert(len(u.Email) >= 5, "email must be at least 5 characters")),
     "address":  u.Address.Validate(),
     "orders":   validator.Required(u.Orders, validator.ValidateMany(u.Orders)),
     "password": validator.Required(u.Password, validator.AssertMap(passwordConds)),
   })
 }

// Validate and print errors as JSON
user := User{}
err := user.Validate()
if err != nil {
  b, _ := json.MarshalIndent(err, "", "  ")
  fmt.Println(string(b))
}
Table-driven scenario testing
scenarios := []struct {
  label string
  user  User
}{
  {label: "missing all required fields", user: User{}},
  {label: "missing address", user: User{Name: "John", Email: "[email protected]", Orders: []Order{{ID: "A123", Amount: 100}}, Password: "Abcdef12"}},
  // ...more scenarios...
}
for _, s := range scenarios {
  fmt.Printf("--- %s ---\n", s.label)
  err := s.user.Validate()
  b, _ := json.MarshalIndent(err, "", "  ")
  fmt.Println(string(b))
}
Composing Optional, Required, Assert, and AssertMap

You can nest and compose these helpers for flexible validation logic:

// Required with nested Assert
err := validator.Required(username, validator.Assert(len(username) >= 3, "username must be at least 3 characters"))

// Optional with nested Assert
err := validator.Optional(bio, validator.Assert(len(bio) <= 100, "bio must be at most 100 characters"))

// Required with AssertMap for multiple rules
conds := map[string]bool{
  "must be at least 8 characters": len(password) >= 8,
  "must contain a number":         containsNumber(password),
  "must contain a capital letter": containsCapital(password),
}
err := validator.Required(password, validator.AssertMap(conds))

You can nest these as deeply as needed, e.g.:

err := validator.Required(field, validator.Assert(len(field) > 0, "cannot be empty"), validator.AssertMap(conds))
Notes on When and WhenMap
  • When returns an error if the condition is true (useful for positive assertions):
    err := validator.When(isDuplicate, "duplicate value detected")
    
  • Assert returns an error if the condition is false (useful for negative assertions):
    err := validator.Assert(isValid, "value is not valid")
    
  • WhenMap aggregates all messages whose condition is true:
    conds := map[string]bool{"field is empty": isEmpty, "field is duplicate": isDuplicate}
    err := validator.WhenMap(conds)
    
  • AssertMap aggregates all messages whose condition is false:
    conds := map[string]bool{"must be positive": value > 0, "must be even": value%2 == 0}
    err := validator.AssertMap(conds)
    

These helpers make it easy to build readable, composable validation logic for any scenario.

Examples

See validator_example_test.go for more usage and scenario examples.

Testing

Run unit and scenario tests:

go test -v ./...

License

MIT

Documentation

Overview

Example (Api_payload_validation)

Example of validating a real-world API payload with nested structs and slices

payload := User{
	Name:     "",
	Email:    "",
	Address:  Address{Street: "", City: "", Zip: "12345"},
	Orders:   []Order{{ID: "", Amount: 0}, {ID: "A123", Amount: 100}},
	Password: "abc",
}
err := payload.Validate()
if err != nil {
	b, _ := json.MarshalIndent(err, "", "  ")
	fmt.Println(string(b))
} else {
	fmt.Println("<nil>")
}
Output:

{
  "address": {
    "city": "required",
    "street": "required"
  },
  "email": "required",
  "name": "required",
  "orders[0]": {
    "amount": "required",
    "id": "required",
    "product": {
      "code": "required",
      "price": "price must be at least 1"
    }
  },
  "orders[1]": {
    "product": {
      "code": "required",
      "price": "price must be at least 1"
    }
  },
  "password": "must be at least 8 characters, must contain a capital letter, must contain a number"
}
Example (Nested_validation)

Example of nested struct validation and error aggregation

p := Parent{
	Name:     "",
	Age:      0,
	Children: []Child{{Name: ""}, {Name: "Alice"}},
}
err := p.Validate()
fmt.Println(err)
Output:

invalid fields: age, children[0], name
Example (Required_optional)

Example of using Required and Optional

fmt.Println(Required(nil))
fmt.Println(Required("foo"))
fmt.Println(Optional(nil, errors.New("fail")))
fmt.Println(Optional("foo", errors.New("fail")))
Output:

required
<nil>
<nil>
fail

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrRequired = errors.New("required")

ErrRequired is returned when a required value is missing or zero.

Functions

func Assert

func Assert(valid bool, msg string, args ...any) error

Assert returns an error with the given message if valid is false, otherwise returns nil.

func AssertMap

func AssertMap(conds map[string]bool) error

AssertMap returns an error listing all messages whose condition is false.

func Map

func Map(m map[string]error) error

Map converts a map of field errors into a structured errorMap, handling nested errors.

Example

Example of Map for field errors

m := map[string]error{"field": errors.New("fail")}
err := Map(m)
fmt.Println(err)
Output:

invalid fields: field

func Optional

func Optional(value any, errs ...error) error

Optional returns nil if the value is zero, otherwise joins additional errors.

func Required

func Required(value any, errs ...error) error

Required returns ErrRequired if the value is zero, otherwise joins additional errors.

func Validate

func Validate[T validatable](v T) error

Validate calls Validate on a validatable type if it is not zero.

func ValidateMany

func ValidateMany[T validatable](value []T) error

ValidateMany validates each item in a slice of validatable types and collects errors. Returns nil if no errors are found, otherwise returns an errorSlice.

Example

Example of ValidateMany with validatable

items := []validatableItem{{myType{true}}, {myType{false}}, {myType{true}}}
err := ValidateMany(items)
fmt.Println(err)
Output:

invalid slice

func ValidateManyFunc

func ValidateManyFunc[T any](items []T, fn func(T) error) error

ValidateManyFunc applies a validation function to each item in a slice and collects errors. Returns nil if no errors are found, otherwise returns an errorSlice.

Example

Example of ValidateManyFunc

items := []int{1, 2, 3}
err := ValidateManyFunc(items, func(i int) error {
	if i%2 == 0 {
		return fmt.Errorf("even: %d", i)
	}
	return nil
})
fmt.Println(err)
Output:

invalid slice

func When

func When(valid bool, msg string, args ...any) error

When returns an error with the given message if valid is true, otherwise returns nil.

func WhenMap

func WhenMap(conds map[string]bool) error

WhenMap returns an error listing all messages whose condition is true.

Types

This section is empty.

Jump to

Keyboard shortcuts

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