valex

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 27, 2026 License: MIT Imports: 3 Imported by: 0

README

Valex

Go Reference CI

Valex is an extensible validation library for Go. It pairs a small, dependency-light validation engine with opt-in packages for a ready-made directive catalog and HTTP form binding, so you depend only on what you use.

Packages

Import Responsibility
github.com/tedla-brandsema/valex The engine: the Validator[T] interface and ValidatorFunc[T] adapter, the ValidatedValue[T] wrapper, MustValidate, the val struct tag (ValidateStruct), RegisterDirective / MustRegisterDirective, and re-exported error types.
github.com/tedla-brandsema/valex/validators A catalog of ready-made val directives (ranges, lengths, URLs, emails, IPs, time, JSON/XML, regex, …). Directives are opt-in — you register the ones you want.
github.com/tedla-brandsema/valex/forms Bind net/http request values into structs and validate them. Kept separate so the core engine never imports net/http.

Features

  • Generic validators — define type-safe validators via the Validator[T] interface or the ValidatorFunc[T] adapter.
  • Validated value wrapperValidatedValue[T] only stores values that pass validation.
  • Tag-based validation — validate struct fields with the val tag and ValidateStruct.
  • Opt-in directive catalog — register only the directives you need from valex/validators.
  • Custom directives — extend the val tag with RegisterDirective (or MustRegisterDirective to fail fast at startup).
  • HTTP form binding — parse and validate requests with valex/forms.
  • Inspectable errors — error types are re-exported from the engine, so you handle them without importing tagex.

Installation

Requires Go 1.22 or later.

go get -u github.com/tedla-brandsema/valex@latest

Programmatic validation

Implement the Validator[T] interface for your type, or adapt a function with ValidatorFunc[T], and use ValidatedValue[T] for guarded assignment:

package main

import (
	"fmt"

	"github.com/tedla-brandsema/valex"
)

func main() {
	// A quick validator from a function.
	nonEmpty := valex.ValidatorFunc[string](func(val string) error {
		if val == "" {
			return fmt.Errorf("string cannot be empty")
		}
		return nil
	})

	vv := valex.ValidatedValue[string]{Validator: nonEmpty}
	if err := vv.Set("hello world"); err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("validated value:", vv.Get())
}

Tag-based validation

The engine ships no directives of its own. Register the ones you need from valex/validators (typically in init), then call ValidateStruct:

package main

import (
	"fmt"

	"github.com/tedla-brandsema/valex"
	"github.com/tedla-brandsema/valex/validators"
)

func init() {
	valex.MustRegisterDirective(&validators.MinLengthValidator{})
	valex.MustRegisterDirective(&validators.EmailValidator{})
	valex.MustRegisterDirective(&validators.IntRangeValidator{})
}

type User struct {
	Name  string `val:"min,size=3"`
	Email string `val:"email"`
	Age   int    `val:"rangeint,min=0,max=120"`
}

func main() {
	if err := valex.ValidateStruct(&User{Name: "Al", Email: "invalid", Age: 200}); err != nil {
		fmt.Println(err)
	}
}

See the validators package for the full catalog, or the table below.

Custom directives

A directive is any tagex.Directive[T] — implement Name, Mode, and Handle, then register it:

package main

import (
	"fmt"

	"github.com/tedla-brandsema/tagex"
	"github.com/tedla-brandsema/valex"
)

type EvenDirective struct{}

func (d *EvenDirective) Name() string              { return "even" }
func (d *EvenDirective) Mode() tagex.DirectiveMode { return tagex.EvalMode }
func (d *EvenDirective) Handle(val int) (int, error) {
	if val%2 != 0 {
		return val, fmt.Errorf("value %d is not even", val)
	}
	return val, nil
}

func main() {
	valex.MustRegisterDirective(&EvenDirective{})

	type Item struct {
		Count int `val:"even"`
	}
	if err := valex.ValidateStruct(&Item{Count: 3}); err != nil {
		fmt.Println(err)
	}
}

HTTP form validation

valex/forms binds request values into a struct using field tags, then validates the val tags. forms.New calls request.ParseForm, which reads both POST bodies and URL query parameters (so GET requests work too). forms.Validate is a convenience wrapper that returns a *forms.Error carrying an HTTP status code; forms.Bind binds without validating when you are outside an HTTP handler.

type Signup struct {
	Name  string `field:"name" val:"min,size=3"`
	Email string `field:"email" val:"email"`
}

func handler(w http.ResponseWriter, r *http.Request) {
	var in Signup
	if err := forms.Validate(r, &in); err != nil {
		var ferr *forms.Error
		errors.As(err, &ferr)
		http.Error(w, err.Error(), ferr.StatusCode())
		return
	}
	// ... use in
}

Error handling

ValidateStruct and the forms helpers return errors you can inspect with errors.As / errors.Is. The engine re-exports the underlying error types, so you do not need to import tagex:

err := valex.ValidateStruct(&User{ /* ... */ })
if err != nil {
	var conv *valex.ConversionError
	switch {
	case errors.As(err, &conv):
		// a parameter value could not be converted
	case errors.Is(err, valex.ErrNoValidator):
		// ...
	}
}

Examples

Runnable programs in examples/ — run one with go run ./examples/<name>:

  • programmatic — validate values in code with ValidatorFunc and ValidatedValue, no tags.
  • validate-struct — register catalog directives and validate a struct with the val tag.
  • custom-directive — extend the val tag with your own directive.
  • forms — bind and validate an net/http request with valex/forms.

Documentation

Full documentation is in docs/:

Package reference and Go testable examples render on pkg.go.dev. Working notes and deferred decisions live in TODO.md.

Built-in directives

From github.com/tedla-brandsema/valex/validators. Register each with valex.RegisterDirective(&XxxValidator{}) before validating.

Validator Type Tag Params (defaults) Description
Generic (programmatic, no tag)
CmpRangeValidator[T] cmp.Ordered - Min, Max Inclusive range for ordered types.
NonZeroValidator[T] any - - Value is not the zero value.
CompositeValidator[T] cmp.Ordered - Validators Runs several validators in order.
Ints
IntRangeValidator int rangeint min, max Inclusive int range.
MinIntValidator int minint min Int >= min.
MaxIntValidator int maxint max Int <= max.
NonNegativeIntValidator int posint - Int is non-negative.
NonPositiveIntValidator int negint - Int is non-positive.
NonZeroIntValidator int !zeroint - Int is not zero.
OneOfIntValidator int oneofint values Int is in values (pipe-separated).
Float64
Float64RangeValidator float64 rangefloat min, max Inclusive float64 range.
MinFloat64Validator float64 minfloat min Float64 >= min.
MaxFloat64Validator float64 maxfloat max Float64 <= max.
NonNegativeFloat64Validator float64 posfloat - Float64 is non-negative.
NonPositiveFloat64Validator float64 negfloat - Float64 is non-positive.
NonZeroFloat64Validator float64 !zerofloat - Float64 is not zero.
OneOfFloat64Validator float64 oneoffloat values Float64 is in values (pipe-separated).
Strings
UrlValidator string url - Valid absolute URL.
EmailValidator string email - Valid email address.
NonEmptyStringValidator string !empty - String is not empty.
MinLengthValidator string min size String length >= size.
MaxLengthValidator string max size String length <= size.
LengthRangeValidator string len min, max String length in inclusive range.
RegexValidator string regex pattern String matches regex.
PrefixValidator string prefix value String has prefix.
SuffixValidator string suffix value String has suffix.
ContainsValidator string contains value String contains substring.
OneOfStringValidator string oneof values String is in values (pipe-separated).
AlphaNumericValidator string alphanum - String is alphanumeric.
MACAddressValidator string mac - Valid MAC address.
IpValidator string ip - Valid IP address.
IPv4Validator string ipv4 - Valid IPv4 address.
IPv6Validator string ipv6 - Valid IPv6 address.
HostnameValidator string hostname - Valid hostname.
IPCIDRValidator string cidr - Valid CIDR notation.
UUIDValidator string uuid version (4) RFC 4122 UUID with optional version.
Base64Validator string base64 - Valid base64 (standard or raw).
HexValidator string hex - Valid hex string (optional 0x).
XMLValidator string xml - Well-formed XML with at least one element.
JSONValidator string json - Valid JSON.
TimeValidator string time format (RFC3339) Valid time for layout (built-in name or raw layout).
Time
NonZeroTimeValidator time.Time !zerotime - Time is not zero.
TimeBeforeValidator time.Time beforetime before Time is before the configured time (RFC3339).
TimeAfterValidator time.Time aftertime after Time is after the configured time (RFC3339).
TimeBetweenValidator time.Time betweentime start, end Time is within the inclusive range (RFC3339).
Duration
PositiveDurationValidator time.Duration posduration - Duration is positive.
NonZeroDurationValidator time.Duration !zeroduration - Duration is not zero.
IP
NonZeroIPValidator net.IP !zeroip - IP is not zero or unspecified.
IPRangeValidator net.IP iprange start, end IP is within the inclusive range.
URL
NonZeroURLValidator url.URL !zerourl - URL is not the zero value.

Status

Valex is pre-1.0 (0.x): the API is still settling, and breaking changes bump the minor version. See the changelog for what changed and the full stability policy, and pin a version.

License

This project is licensed under the MIT License – see the LICENSE file for details.

Documentation

Overview

Package valex is a small value-validation engine built on the tagex struct-tag processor.

Requires Go 1.22 or later.

It supports two primary workflows:

  1. Programmatic validation using Validator or ValidatorFunc, with ValidatedValue for guarded assignment and MustValidate for fail-fast use.
  2. Struct-tag validation using the "val" tag and ValidateStruct. Register directives with MustRegisterDirective (or RegisterDirective, which returns an error instead of panicking); pass additional tagex.Tag values to ValidateStruct to process multiple tags in a single pass.

The engine ships no directives of its own. Ready-made validators live in the github.com/tedla-brandsema/valex/validators subpackage; register the ones you need with MustRegisterDirective. HTTP request binding and validation live in the github.com/tedla-brandsema/valex/forms subpackage, which keeps net/http out of the core engine.

Concurrency

Registering a directive and ValidateStruct are safe for concurrent use: the "val" tag's directive registry is guarded by a mutex, and ValidateStruct only reads it. The intended pattern is to register directives once at startup (typically in an init function) and validate from many goroutines thereafter. Registering while other goroutines validate is safe but unusual.

Registries

The package-level RegisterDirective, MustRegisterDirective, and ValidateStruct share one process-global default registry — like flag.CommandLine or http.DefaultServeMux, it belongs to the application, which registers once at startup and validates anywhere.

Libraries, and tests that need isolation, should create their own with NewRegistry instead of touching the global. Each Registry has an independent directive set, so two can hold the same directive name without colliding. Register on one with the free functions RegisterDirectiveTo / MustRegisterDirectiveTo (free functions because Go methods can't be generic), and validate with its ValidateStruct method.

Index

Examples

Constants

View Source
const (
	StageInput     = tagex.StageInput
	StagePre       = tagex.StagePre
	StageDirective = tagex.StageDirective
	StageParam     = tagex.StageParam
	StagePost      = tagex.StagePost
	StageStruct    = tagex.StageStruct
)

Processing stages, re-exported from tagex for use with Stage and ProcessError.

Variables

View Source
var ErrNoValidator = errors.New("valex: no validator set")

ErrNoValidator is returned by ValidatedValue.Set when no Validator is configured.

Functions

func FieldErrors added in v0.2.0

func FieldErrors(err error) map[string]error

FieldErrors flattens an error returned by ValidateStructAll into a map from field path to the error for that field, keeping the first error seen per field.

The keys are struct field paths — the same values ProcessError.FieldPath carries, e.g. "Email" or "Items[2].SKU" — not request keys or display names; translate them yourself when rendering. It walks errors.Join trees, so it works on the accumulated result of ValidateStructAll. A nil error yields a nil map. Field-less errors (such as *InvalidTargetError) are omitted, so the original error stays authoritative — check err != nil first, then render the map on top.

func MustRegisterDirective

func MustRegisterDirective[T any](d tagex.Directive[T])

MustRegisterDirective is like RegisterDirective but panics if registration fails. It is the convenient choice for registering directives once at startup (typically in an init function), where a duplicate or empty directive name is a programming error that should fail fast.

func MustRegisterDirectiveTo added in v0.2.0

func MustRegisterDirectiveTo[T any](r *Registry, d tagex.Directive[T])

MustRegisterDirectiveTo is like RegisterDirectiveTo but panics if registration fails — the convenient choice for registering directives once at startup.

func MustValidate

func MustValidate[T any](val T, v Validator[T]) T

MustValidate validates a value or panics if validation fails.

func RegisterDirective

func RegisterDirective[T any](d tagex.Directive[T]) error

RegisterDirective registers a directive on the default registry for use with the "val" struct tag. It returns *EmptyDirectiveNameError if the directive's Name is blank, or *DuplicateDirectiveError if that name is already registered (it does not overwrite). Use MustRegisterDirective to panic on these instead, which is usually what you want when registering at startup.

Example

RegisterDirective extends the "val" tag with a custom directive.

package main

import (
	"fmt"

	"github.com/tedla-brandsema/tagex"
	"github.com/tedla-brandsema/valex"
)

// evenDirective is a custom "val" directive that accepts only even ints.
// A directive is registered as a pointer so tagex can populate its parameters.
type evenDirective struct{}

func (*evenDirective) Name() string              { return "even" }
func (*evenDirective) Mode() tagex.DirectiveMode { return tagex.EvalMode }
func (*evenDirective) Handle(n int) (int, error) {
	if n%2 != 0 {
		return n, fmt.Errorf("value %d is not even", n)
	}
	return n, nil
}

func main() {
	valex.RegisterDirective(&evenDirective{})

	type Ticket struct {
		Seats int `val:"even"`
	}

	fmt.Println(valex.ValidateStruct(&Ticket{Seats: 4}))
	fmt.Println(valex.ValidateStruct(&Ticket{Seats: 3}))
}
Output:
<nil>
tag "val" error: directive processing field "Seats" directive "even": value 3 is not even

func RegisterDirectiveTo added in v0.2.0

func RegisterDirectiveTo[T any](r *Registry, d tagex.Directive[T]) error

RegisterDirectiveTo registers a directive on r. It is a free function rather than a method because Go methods cannot have type parameters. It returns *EmptyDirectiveNameError if the directive's Name is blank, or *DuplicateDirectiveError if that name is already registered on r; use MustRegisterDirectiveTo to panic on these instead.

func ValidateStruct

func ValidateStruct(data any, tags ...*tagex.Tag) error

ValidateStruct validates struct fields using the default registry's "val" directives. It returns nil when the struct is valid. Additional tagex.Tag values can be provided to process more tags in the same pass.

Example

ValidateStruct validates fields via the "val" tag. Directives are opt-in: register the ones you use (here from the valex/validators catalog) first.

package main

import (
	"fmt"

	"github.com/tedla-brandsema/valex"
	"github.com/tedla-brandsema/valex/validators"
)

func main() {
	valex.RegisterDirective(&validators.EmailValidator{})
	valex.RegisterDirective(&validators.IntRangeValidator{})

	type User struct {
		Email string `val:"email"`
		Age   int    `val:"rangeint,min=0,max=120"`
	}

	fmt.Println(valex.ValidateStruct(&User{Email: "gopher@example.com", Age: 30}))
	fmt.Println(valex.ValidateStruct(&User{Email: "gopher@example.com", Age: 200}))
}
Output:
<nil>
tag "val" error: directive processing field "Age" directive "rangeint": value 200 is out of range [0, 120]

func ValidateStructAll added in v0.2.0

func ValidateStructAll(data any, tags ...*tagex.Tag) error

ValidateStructAll is like ValidateStruct but does not stop at the first failure: it validates every field against the default registry and returns errors.Join of the per-field errors (nil when all pass). Use FieldErrors to turn the result into a map keyed by field path.

Types

type ConversionError

type ConversionError = tagex.ConversionError

ConversionError is returned when a raw parameter value cannot be converted.

type DirectiveParseError

type DirectiveParseError = tagex.DirectiveParseError

DirectiveParseError is returned when a tag value omits the directive name.

type DuplicateDirectiveError

type DuplicateDirectiveError = tagex.DuplicateDirectiveError

DuplicateDirectiveError is returned by RegisterDirective when the directive name is already registered.

type EmptyDirectiveNameError

type EmptyDirectiveNameError = tagex.EmptyDirectiveNameError

EmptyDirectiveNameError is returned by RegisterDirective for a directive with a blank Name.

type FieldAccessError

type FieldAccessError = tagex.FieldAccessError

FieldAccessError is returned when a struct field cannot be read via reflection.

type FieldSetError

type FieldSetError = tagex.FieldSetError

FieldSetError is returned when a struct field cannot be set via reflection.

type HandleError

type HandleError = tagex.HandleError

HandleError wraps an error returned by a directive's Handle method.

type HookError

type HookError = tagex.HookError

HookError wraps an error returned by a pre- or post-processing hook.

type InvalidTargetError

type InvalidTargetError = tagex.InvalidTargetError

InvalidTargetError is returned when ValidateStruct gets a value that is not a pointer to a struct.

type MaxDepthError

type MaxDepthError = tagex.MaxDepthError

MaxDepthError is returned when processing recurses past the nesting limit (usually cyclic data).

type MissingParamError

type MissingParamError = tagex.MissingParamError

MissingParamError is returned when a required parameter is absent.

type NilTagError

type NilTagError = tagex.NilTagError

NilTagError is returned when a nil tag is processed.

type ParamConflictError

type ParamConflictError = tagex.ParamConflictError

ParamConflictError is returned when a parameter sets both required and default.

type ParamParseError

type ParamParseError = tagex.ParamParseError

ParamParseError is returned for a malformed "key=value" parameter pair.

type ProcessError

type ProcessError = tagex.ProcessError

ProcessError describes a failure while processing a single struct field.

type Registry added in v0.2.0

type Registry struct {
	// contains filtered or unexported fields
}

Registry is an isolated set of "val" directives. Most programs use the package-level RegisterDirective / MustRegisterDirective / ValidateStruct, which share one process-global default registry. Create your own with NewRegistry when you need an independent one — for test isolation, or to run two differently-configured validators in the same process.

func NewRegistry added in v0.2.0

func NewRegistry() *Registry

NewRegistry returns a new, empty Registry with its own directive set.

func (*Registry) ValidateStruct added in v0.2.0

func (r *Registry) ValidateStruct(data any, tags ...*tagex.Tag) error

ValidateStruct validates struct fields against the registry's "val" directives. It returns nil when the struct is valid. Additional tagex.Tag values can be provided to process more tags in the same pass.

func (*Registry) ValidateStructAll added in v0.2.0

func (r *Registry) ValidateStructAll(data any, tags ...*tagex.Tag) error

ValidateStructAll is like ValidateStruct but does not stop at the first failure: it validates every field and returns errors.Join of the per-field errors (nil when all pass). Use FieldErrors to turn the result into a map keyed by field path.

type Stage

type Stage = tagex.Stage

Stage identifies the processing stage at which an error occurred.

type TagError

type TagError = tagex.TagError

TagError wraps all errors produced for a given struct-tag key.

type TypeMismatchError

type TypeMismatchError = tagex.TypeMismatchError

TypeMismatchError is returned when a field's type does not match the directive.

type UnknownDirectiveError

type UnknownDirectiveError = tagex.UnknownDirectiveError

UnknownDirectiveError is returned when a tag names an unregistered directive.

type UnsupportedParamTypeError

type UnsupportedParamTypeError = tagex.UnsupportedParamTypeError

UnsupportedParamTypeError is returned for a parameter of an unsupported type.

type ValidatedValue

type ValidatedValue[T any] struct {
	Validator Validator[T]
	// contains filtered or unexported fields
}

ValidatedValue stores a value and validates updates with the provided Validator. It is an in-memory guard, not a serialization type: the stored value is unexported and a decoder has no way to supply the Validator, so it does not round-trip through encoding/json. For serialized or request input, validate with ValidateStruct (the "val" tag) instead.

Example

ValidatedValue stores a value only when it passes the configured Validator, leaving the previous value in place on failure.

package main

import (
	"errors"
	"fmt"

	"github.com/tedla-brandsema/valex"
)

func main() {
	positive := valex.ValidatorFunc[int](func(n int) error {
		if n <= 0 {
			return errors.New("must be positive")
		}
		return nil
	})

	v := valex.ValidatedValue[int]{Validator: positive}

	if err := v.Set(42); err == nil {
		fmt.Println("stored:", v.Get())
	}
	if err := v.Set(-1); err != nil {
		fmt.Println("rejected -1:", err)
	}
	fmt.Println("still:", v.Get())
}
Output:
stored: 42
rejected -1: must be positive
still: 42

func (*ValidatedValue[T]) Get

func (v *ValidatedValue[T]) Get() T

Get returns the stored value.

func (*ValidatedValue[T]) Set

func (v *ValidatedValue[T]) Set(val T) error

Set validates and stores the value.

func (*ValidatedValue[T]) String

func (v *ValidatedValue[T]) String() string

String returns the string representation of the stored value.

type Validator

type Validator[T any] interface {
	Validate(val T) error
}

Validator defines the behavior for validating a value of type T. A nil error means the value is valid; a non-nil error reports why it is invalid.

type ValidatorFunc

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

ValidatorFunc adapts a function to the Validator interface.

Example

ValidatorFunc adapts a plain function into a Validator. A nil return means the value is valid.

package main

import (
	"errors"
	"fmt"

	"github.com/tedla-brandsema/valex"
)

func main() {
	nonEmpty := valex.ValidatorFunc[string](func(s string) error {
		if s == "" {
			return errors.New("must not be empty")
		}
		return nil
	})

	fmt.Println(nonEmpty.Validate("hello"))
	fmt.Println(nonEmpty.Validate(""))
}
Output:
<nil>
must not be empty

func (ValidatorFunc[T]) Validate

func (p ValidatorFunc[T]) Validate(val T) error

Validate calls the underlying function.

Directories

Path Synopsis
examples
chained command
Chained applies several directives to one field by separating them with ';'.
Chained applies several directives to one field by separating them with ';'.
custom-directive command
Custom-directive extends the "val" tag with a directive of your own.
Custom-directive extends the "val" tag with a directive of your own.
forms command
Forms binds an net/http request into a struct and validates its "val" tags.
Forms binds an net/http request into a struct and validates its "val" tags.
programmatic command
Programmatic validates values in code with ValidatorFunc and ValidatedValue, without any struct tags.
Programmatic validates values in code with ValidatorFunc and ValidatedValue, without any struct tags.
validate-struct command
Validate-struct registers catalog directives and validates a struct with the "val" tag.
Validate-struct registers catalog directives and validates a struct with the "val" tag.
Package forms binds HTTP request values into structs and validates them with the valex engine's "val" tag.
Package forms binds HTTP request values into structs and validates them with the valex engine's "val" tag.
internal
stub
Package stub provides shared test fixtures for the valex module.
Package stub provides shared test fixtures for the valex module.
Package validators provides a catalog of ready-made validation directives for the valex engine.
Package validators provides a catalog of ready-made validation directives for the valex engine.

Jump to

Keyboard shortcuts

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