criterio

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jan 23, 2026 License: MIT Imports: 11 Imported by: 0

README

criterio

A composable validation library for Go. Validators are functions that can be combined, reused, and composed to build complex validation logic.

go get github.com/hay-kot/criterio

Requires Go 1.23+

Core Concepts

All validators implement a single type:

type Validator[T any] func(val T) error

Validators return nil on success or an error describing the failure.

API Overview

Running Validators
// Stop on first failure
err := criterio.Run("email", user.Email,
    criterio.Required[string],
    criterio.StrEmail,
)

// Collect all failures
err := criterio.RunAll("email", user.Email,
    criterio.Required[string],
    criterio.StrMin(5),
    criterio.StrEmail,
)
Reusable Validators
validateEmail := criterio.New("email",
    criterio.Required[string],
    criterio.StrEmail,
)

if err := validateEmail(input); err != nil {
    // handle error
}
Struct Validation
func (u User) Validate() error {
    return criterio.ValidateStruct(
        criterio.Run("name", u.Name,
            criterio.Required[string],
            criterio.StrBetween(2, 100),
        ),
        criterio.Run("email", u.Email,
            criterio.Required[string],
            criterio.StrEmail,
        ),
        criterio.Run("age", u.Age,
            criterio.Min(0),
            criterio.Max(150),
        ),
        criterio.Nest("address", u.Address.Validate()),
    )
}
Conditional Validation
// Only validate if condition is true
criterio.When(u.IsPremium, criterio.Required[string])

// Skip validation if condition is true
criterio.SkipIf(u.IsGuest, criterio.StrEmail)
Slice Validation
func (u User) Validate() error {
    return criterio.ValidateStruct(
        criterio.Run("name", u.Name, criterio.Required[string]),
        criterio.ValidateSlice("addresses", u.Addresses, Address.Validate),
    )
}
// Error: "addresses[0].street: is required"

For simple slices:

criterio.ValidateSlice("emails", emails, criterio.StrEmail)
// Error: "emails[1]: must be a valid email address"
Combinators
// Pass if any validator passes
criterio.Or(criterio.StrEmail, criterio.StrNumeric)

// Invert a validator
criterio.Not(criterio.StrNumeric, "cannot be all numbers")

Available Validators

General
Validator Description
Required[T comparable] Value is non-zero
OneOf[T comparable](vals...) Value is in allowed set
Numeric (ordered types)
Validator Description
Min(n) Value >= n
Max(n) Value <= n
Between(low, high) Value in range [low, high]
Positive Value > 0
Negative Value < 0
NonZero Value != 0
MultipleOf(n) Value divisible by n
Strings
Validator Description
StrNotEmpty Non-empty after trimming whitespace
StrMin(n) At least n characters
StrMax(n) At most n characters
StrBetween(low, high) Length in range [low, high]
StrMatches(re) Matches regex pattern
StrEmail Valid email format
StrURL Valid URL with scheme and host
StrUUID Valid UUID format
StrAlpha Letters only
StrAlphanumeric Letters and numbers only
StrNumeric Digits only
StrContains(s) Contains substring
StrHasPrefix(s) Starts with prefix
StrHasSuffix(s) Ends with suffix
StrNoWhitespace No whitespace characters
StrLowercase All lowercase
StrUppercase All uppercase
StrOneOf(vals...) Value is in allowed set
Slices
Validator Description
SliceNotEmpty At least one element
SliceLenMin(n) At least n elements
SliceLenMax(n) At most n elements
SliceLenBetween(low, high) Length in range [low, high]
SliceUnique All elements are unique
SliceEach(v) Apply validator to each element
Maps
Validator Description
MapNotEmpty At least one entry
MapLenMin(n) At least n entries
MapLenMax(n) At most n entries
MapLenBetween(low, high) Entry count in range [low, high]
MapKeys(v) Apply validator to all keys
MapValues(v) Apply validator to all values
Network
Validator Description
NetIP Valid IPv4 or IPv6 address
NetIPv4 Valid IPv4 address
NetIPv6 Valid IPv6 address
NetCIDR Valid CIDR notation
NetHost Valid hostname
NetPort Valid port number (1-65535)
NetPortStr Valid port number as string
Time
Validator Description
TimeFuture Time is in the future
TimePast Time is in the past
TimeAfter(t) Time is after t
TimeBefore(t) Time is before t
TimeBetween(start, end) Time in range [start, end]
Duration
Validator Description
DurPositive Duration > 0
DurMin(d) Duration >= d
DurMax(d) Duration <= d
DurBetween(low, high) Duration in range [low, high]

Error Handling

Errors are returned as FieldErrors, a slice of FieldError:

type FieldError struct {
    Field string
    Err   error
}
Error Output

Single field error:

email: must be a valid email address

Multiple errors (from RunAll or ValidateStruct):

validation failed: name: must be between 2 and 100 characters; email: must be a valid email address

Nested struct errors use dot notation:

address.street: is required
address.zip: must contain only digits

Slice errors use bracket notation:

addresses[0].street: is required
emails[1]: must be a valid email address
Accessing Individual Errors
if err := user.Validate(); err != nil {
    if fieldErrs, ok := err.(criterio.FieldErrors); ok {
        for _, fe := range fieldErrs {
            fmt.Printf("%s: %s\n", fe.Field, fe.Err.Error())
        }
    }
}

Customization

String validators use exported regex patterns that can be modified globally:

var (
    EmailRegex        *regexp.Regexp
    UUIDRegex         *regexp.Regexp
    AlphaRegex        *regexp.Regexp
    AlphanumericRegex *regexp.Regexp
    NumericRegex      *regexp.Regexp
    WhitespaceRegex   *regexp.Regexp
)

Override in an init function to change validation behavior:

func init() {
    criterio.EmailRegex = regexp.MustCompile(`your-pattern`)
}

Benchmarks

Comparison against manual validation and go-playground/validator.

Run task bench:readme to regenerate.

Key takeaways:

  • Zero allocations on valid inputs - the happy path is allocation-free
  • ~2x faster than go-playground/validator for struct validation
  • Comparable to manual validation for simple cases, with structured error handling
  • Error paths are competitive with go-playground/validator for typical structs (User: 1077ns/453B vs 1135ns/665B)
  • Complex nested validation has more allocations due to FieldErrors merging, but provides richer error context with field paths
Benchmark ns/op B/op allocs/op
Email_Manual_Valid 367.7 0 0
Email_Playground_Valid 653.0 0 0
Email_Criterio_Valid 375.4 0 0
Email_Manual_Invalid 365.9 16 1
Email_Playground_Invalid 696.5 185 3
Email_Criterio_Invalid 411.2 56 2
User_Manual_Valid 232.0 88 5
User_Playground_Valid 873.1 48 1
User_Criterio_Valid 412.1 0 0
User_Manual_Invalid 19.84 16 1
User_Playground_Invalid 1135 665 11
User_Criterio_Invalid 1077 453 15
Order_Manual_Valid 540.1 88 5
Order_Playground_Valid 1949 235 5
Order_Criterio_Valid 816.2 0 0
Order_Manual_Invalid 23.67 16 1
Order_Playground_Invalid 2169 2186 25
Order_Criterio_Invalid 3268 2525 54
Slice_Manual_Valid 4.149 0 0
Slice_Playground_Valid 457.8 104 7
Slice_Criterio_Valid 79.43 40 2
User_Manual_Parallel 79.44 88 5
User_Playground_Parallel 232.5 49 1
User_Criterio_Parallel 92.03 0 0

License

MIT

Documentation

Overview

Package criterio provides utilities for marking and handling validation errors.

Index

Constants

This section is empty.

Variables

View Source
var (
	EmailRegex        = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
	UUIDRegex         = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
	AlphaRegex        = regexp.MustCompile(`^[a-zA-Z]+$`)
	AlphanumericRegex = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
	NumericRegex      = regexp.MustCompile(`^[0-9]+$`)
	WhitespaceRegex   = regexp.MustCompile(`\s`)
)

Default regex patterns used by string validators. These can be modified to change validation behavior globally.

WARNING: Modifying these affects all validations application-wide. Concurrent modification is not thread-safe. If you need to customize patterns, do so at program initialization before any validation occurs.

View Source
var (
	HostnameRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$`)
)

Default regex patterns used by network validators. These can be modified to change validation behavior globally.

WARNING: Modifying these affects all validations application-wide. Concurrent modification is not thread-safe. If you need to customize patterns, do so at program initialization before any validation occurs.

Functions

func DurPositive

func DurPositive(val time.Duration) error

DurPositive validates that a duration is positive (greater than zero).

func Nest

func Nest(field string, err error) error

Nest prefixes all field errors with a parent field name using dot notation. Useful for nested struct validation to create paths like "address.street".

Non-FieldErrors errors are wrapped with the field name as context.

func NetCIDR

func NetCIDR(val string) error

NetCIDR validates that a string is valid CIDR notation.

func NetHost

func NetHost(val string) error

NetHost validates that a string is a valid hostname. Uses HostnameRegex which can be modified to change validation globally.

func NetIP

func NetIP(val string) error

NetIP validates that a string is a valid IP address (IPv4 or IPv6).

func NetIPv4

func NetIPv4(val string) error

NetIPv4 validates that a string is a valid IPv4 address.

func NetIPv6

func NetIPv6(val string) error

NetIPv6 validates that a string is a valid IPv6 address.

func NetPort

func NetPort(val int) error

NetPort validates that an integer is a valid port number (1-65535).

func NetPortStr

func NetPortStr(val string) error

NetPortStr validates that a string represents a valid port number (1-65535).

func New

func New[T any](field string, validators ...Validator[T]) func(val T) error

New creates a reusable validator function for a specific field. Stops on the first validation failure.

func NewAll

func NewAll[T any](field string, validators ...Validator[T]) func(val T) error

NewAll creates a reusable validator function for a specific field. Collects all validation failures.

func Required

func Required[T comparable](val T) error

Required validates that a value is non-zero.

func Run

func Run[T any](field string, val T, validators ...Validator[T]) error

Run executes validators against the value, stopping on the first failure. Returns nil if all validators pass, or a FieldErrors with the first failure.

func RunAll

func RunAll[T any](field string, val T, validators ...Validator[T]) error

RunAll executes all validators against the value, collecting all failures. Returns nil if all validators pass, or a FieldErrors containing all failures.

func StrAlpha

func StrAlpha(val string) error

StrAlpha validates that a string contains only letters. Uses AlphaRegex which can be modified to change validation globally.

func StrAlphanumeric

func StrAlphanumeric(val string) error

StrAlphanumeric validates that a string contains only letters and numbers. Uses AlphanumericRegex which can be modified to change validation globally.

func StrEmail

func StrEmail(val string) error

StrEmail validates that a string is a valid email address. Uses EmailRegex which can be modified to change validation globally.

func StrLowercase

func StrLowercase(val string) error

StrLowercase validates that a string is all lowercase.

func StrNoWhitespace

func StrNoWhitespace(val string) error

StrNoWhitespace validates that a string contains no whitespace. Uses WhitespaceRegex which can be modified to change validation globally.

func StrNotEmpty

func StrNotEmpty(val string) error

StrNotEmpty validates that a string is not empty or whitespace-only. Note: This trims whitespace before checking, so " " is considered empty. Use Required[string]() if you only want to reject the zero value "".

func StrNumeric

func StrNumeric(val string) error

StrNumeric validates that a string contains only digits. Uses NumericRegex which can be modified to change validation globally.

func StrURL

func StrURL(val string) error

StrURL validates that a string is a valid URL. Validates that the string can be parsed as a URL with a scheme and host.

func StrUUID

func StrUUID(val string) error

StrUUID validates that a string is a valid UUID format. Uses UUIDRegex which can be modified to change validation globally.

func StrUppercase

func StrUppercase(val string) error

StrUppercase validates that a string is all uppercase.

func ValidateSlice

func ValidateSlice[T any](field string, items []T, validate func(T) error) error

ValidateSlice validates each element in a slice using the provided function. Errors are prefixed with bracket notation (e.g., "items[0].name: is required").

func ValidateStruct

func ValidateStruct(validations ...error) error

ValidateStruct combines multiple field validation results into a single error. Pass the results of Run/RunAll calls for each field.

Non-FieldErrors errors (e.g., from external libraries) are included with an empty field name. For proper field context, wrap external errors with Nest or use Run/RunAll.

Types

type FieldError

type FieldError struct {
	Field string
	Err   error
}

FieldError represents a validation error for a specific field.

func NewFieldError

func NewFieldError(field string, err error) FieldError

NewFieldError creates a single FieldError.

func (FieldError) Error

func (e FieldError) Error() string

Error returns the error message for this field error.

func (FieldError) Unwrap

func (e FieldError) Unwrap() error

Unwrap returns the underlying error for use with errors.Is and errors.As.

type FieldErrors

type FieldErrors []FieldError

FieldErrors is a collection of field validation errors that implements the error interface.

func NewFieldErrors

func NewFieldErrors(field string, err error) FieldErrors

NewFieldErrors creates a new FieldErrors from a single field error.

func (FieldErrors) Error

func (e FieldErrors) Error() string

Error returns a human-readable error message combining all field errors.

type FieldErrorsBuilder

type FieldErrorsBuilder []FieldError

FieldErrorsBuilder is used to build a collection of field validation errors. It does not implement the error interface to prevent accidental returns without calling ToError().

func (FieldErrorsBuilder) Append

func (b FieldErrorsBuilder) Append(field string, err error) FieldErrorsBuilder

Append adds a field error to the collection. Returns the updated FieldErrorsBuilder for convenient chaining.

Example usage:

var errs criterio.FieldErrorsBuilder
errs = errs.Append("name", errRequired)
errs = errs.Append("duration", errPositive)
return errs.ToError()

func (FieldErrorsBuilder) ToError

func (b FieldErrorsBuilder) ToError() error

ToError converts FieldErrorsBuilder to an error. Returns nil if there are no errors, preventing the empty-slice-as-non-nil-error issue.

type Integer

type Integer interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

Integer is a constraint for all integer types.

type SignedNumber

type SignedNumber interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
}

SignedNumber is a constraint for signed numeric types that can be negative.

type Validator

type Validator[T any] func(val T) error

Validator validates a value and returns an error if validation fails.

func Between

func Between[T cmp.Ordered](low, high T) Validator[T]

Between returns a validator that checks if a value is between low and high (inclusive). Works with any ordered type (integers, floats, strings, etc.).

func DurBetween

func DurBetween(low, high time.Duration) Validator[time.Duration]

DurBetween returns a validator that checks if a duration is between low and high (inclusive).

func DurMax

func DurMax(max time.Duration) Validator[time.Duration]

DurMax returns a validator that checks if a duration is at most max.

func DurMin

func DurMin(min time.Duration) Validator[time.Duration]

DurMin returns a validator that checks if a duration is at least min.

func MapKeys

func MapKeys[K comparable, V any](validator Validator[K]) Validator[map[K]V]

MapKeys returns a validator that applies a validator to each key in a map. Note: Map iteration order is non-deterministic in Go. When multiple keys fail validation, the error returned may vary between runs.

func MapLenBetween

func MapLenBetween[K comparable, V any](low, high int) Validator[map[K]V]

MapLenBetween returns a validator that checks if a map length is between low and high (inclusive).

func MapLenMax

func MapLenMax[K comparable, V any](max int) Validator[map[K]V]

MapLenMax returns a validator that checks if a map has at most max entries.

func MapLenMin

func MapLenMin[K comparable, V any](min int) Validator[map[K]V]

MapLenMin returns a validator that checks if a map has at least min entries.

func MapNotEmpty

func MapNotEmpty[K comparable, V any]() Validator[map[K]V]

MapNotEmpty returns a validator that checks if a map has at least one entry.

func MapValues

func MapValues[K comparable, V any](validator Validator[V]) Validator[map[K]V]

MapValues returns a validator that applies a validator to each value in a map. Note: Map iteration order is non-deterministic in Go. When multiple values fail validation, the error returned may vary between runs.

func Max

func Max[T cmp.Ordered](max T) Validator[T]

Max returns a validator that checks if a value is at most max. Works with any ordered type (integers, floats, strings, etc.).

func Min

func Min[T cmp.Ordered](min T) Validator[T]

Min returns a validator that checks if a value is at least min. Works with any ordered type (integers, floats, strings, etc.).

func MultipleOf

func MultipleOf[T Integer](n T) Validator[T]

MultipleOf returns a validator that checks if a value is divisible by n. Works with integer types only. Panics if n is zero.

func Negative

func Negative[T SignedNumber]() Validator[T]

Negative returns a validator that checks if a value is less than zero. Works with signed numeric types (integers and floats).

func NonZero

func NonZero[T SignedNumber]() Validator[T]

NonZero returns a validator that checks if a value is not zero. Works with signed numeric types (integers and floats).

func Not

func Not[T any](v Validator[T], msg string) Validator[T]

Not returns a validator that inverts the result of another validator. Passes if the wrapped validator fails, fails if the wrapped validator passes.

func OneOf

func OneOf[T comparable](allowed ...T) Validator[T]

OneOf returns a validator that checks if a value is one of the allowed values. Works with any comparable type. Automatically uses slice iteration for small sets (≤10) and map lookup for larger sets for optimal performance. Panics if no allowed values are provided.

func Or

func Or[T any](validators ...Validator[T]) Validator[T]

Or returns a validator that passes if any of the validators pass. Returns the last error if all validators fail. Panics if no validators are provided.

func Positive

func Positive[T SignedNumber]() Validator[T]

Positive returns a validator that checks if a value is greater than zero. Works with signed numeric types (integers and floats).

func SkipIf

func SkipIf[T any](condition bool, validators ...Validator[T]) Validator[T]

SkipIf returns a validator that skips validation if the condition is true. If the condition is true, validation passes without running validators. Panics if no validators are provided.

func SliceEach

func SliceEach[T any](validator Validator[T]) Validator[[]T]

SliceEach returns a validator that applies a validator to each element in a slice.

func SliceLenBetween

func SliceLenBetween[T any](low, high int) Validator[[]T]

SliceLenBetween returns a validator that checks if a slice length is between low and high (inclusive).

func SliceLenMax

func SliceLenMax[T any](max int) Validator[[]T]

SliceLenMax returns a validator that checks if a slice has at most max elements.

func SliceLenMin

func SliceLenMin[T any](min int) Validator[[]T]

SliceLenMin returns a validator that checks if a slice has at least min elements.

func SliceNotEmpty

func SliceNotEmpty[T any]() Validator[[]T]

SliceNotEmpty returns a validator that checks if a slice has at least one element.

func SliceUnique

func SliceUnique[T comparable]() Validator[[]T]

SliceUnique returns a validator that checks if all elements in a slice are unique.

func StrBetween

func StrBetween(low, high int) Validator[string]

StrBetween returns a validator that checks if a string length is between low and high (inclusive). Counts characters (runes), not bytes.

func StrContains

func StrContains(substr string) Validator[string]

StrContains returns a validator that checks if a string contains a substring.

func StrHasPrefix

func StrHasPrefix(prefix string) Validator[string]

StrHasPrefix returns a validator that checks if a string starts with a prefix.

func StrHasSuffix

func StrHasSuffix(suffix string) Validator[string]

StrHasSuffix returns a validator that checks if a string ends with a suffix.

func StrMatches

func StrMatches(re *regexp.Regexp) Validator[string]

StrMatches returns a validator that checks if a string matches the provided regex.

func StrMax

func StrMax(max int) Validator[string]

StrMax returns a validator that checks if a string has at most max characters (runes).

func StrMin

func StrMin(min int) Validator[string]

StrMin returns a validator that checks if a string has at least min characters (runes).

func StrOneOf

func StrOneOf(allowed ...string) Validator[string]

StrOneOf returns a validator that checks if a string is one of the allowed values. Automatically uses slice iteration for small sets (≤10) and map lookup for larger sets. Panics if no allowed values are provided.

func TimeAfter

func TimeAfter(t time.Time) Validator[time.Time]

TimeAfter returns a validator that checks if a time is after a given time.

func TimeBefore

func TimeBefore(t time.Time) Validator[time.Time]

TimeBefore returns a validator that checks if a time is before a given time.

func TimeBetween

func TimeBetween(start, end time.Time) Validator[time.Time]

TimeBetween returns a validator that checks if a time is between start and end (inclusive).

func TimeFuture

func TimeFuture() Validator[time.Time]

TimeFuture returns a validator that checks if a time is in the future.

func TimePast

func TimePast() Validator[time.Time]

TimePast returns a validator that checks if a time is in the past.

func When

func When[T any](condition bool, validators ...Validator[T]) Validator[T]

When returns a validator that only runs if the condition is true. If the condition is false, validation passes without running validators. Panics if no validators are provided.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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