Files
validate/README.md
T
claudeandClaude Sonnet 5 164dcbf367
CI / test (pull_request) Successful in 34s
CI / test (push) Successful in 37s
length: add MinLengthBytes / MaxLengthBytes
MinLength/MaxLength now count runes, leaving no way to bound a string by
its byte size -- still wanted for fixed-width columns and wire-format
fields. Add the byte-counting pair alongside them, with their own "%d
bytes" sentinels (ErrMustHaveMoreBytes / ErrMustHaveFewerBytes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:48:29 +01:00

3.0 KiB

Go Validate

Small, composable value validators for Go. A validator is just a func(T) error — it returns nil or an error. You compose them with All or Collect and drop in plain closures wherever you need something custom. No struct tags, no reflection, no dependencies outside the standard library.

username := validate.All(
	validate.MinLength(3),
	validate.MaxLength(16),
	validate.Chars("abcdefghijklmnopqrstuvwxyz0123456789_"),
	func(s string) error {
		if strings.HasPrefix(s, "_") {
			return errors.New("must not start with an underscore")
		}
		return nil
	},
)

if err := username(input); err != nil {
	// errors.Is(err, validate.ErrDisallowedChars) still works
}

Why this and not one of the established packages

Package Style
go-playground/validator struct tags (validate:"required,email") driven by reflection
go-ozzo/ozzo-validation rules composed in code, through a Rule interface over interface{}
asaskevich/govalidator a bag of IsEmail / IsURL string helpers

This package is the code-composition style (closest to ozzo-validation) with two constraints held on purpose:

  • Validators are ordinary generic functions. In, Equal and the rest are type-checked by the compiler — pass the wrong type and it does not build. The tag/reflection libraries can't do this; they predate generics.
  • Errors are plain sentinels. errors.Is(err, ErrInvalidEmail) composes with the standard errors package. There is no bespoke ValidationErrors type to learn. Err matches any error from the package.

Everything is value-level: there is no struct walker. You wire fields together yourself (a few lines) and decide how to present the result.

Fail fast or collect everything

// stops at the first failure
validate.All(rules...)

// runs every rule, joins the failures with errors.Join;
// errors.Is still matches each one
validate.Collect(rules...)

Building blocks

Group Functions
Compose All, Collect
Presence / equality Required, Equal, In, NotIn
String length & content MinLength, MaxLength (runes), MinLengthBytes, MaxLengthBytes, Chars, ExceptChars, Prefix, Suffix, Contains, Match
Formats Email, URL, UUID
Numbers Min, Max, MinFloat32, MaxFloat32, MinFloat64, MaxFloat64
Slices MinSize, MaxSize
Errors Error, NewError, Err

Each returns (or is) a func(T) error, so anything you write with the same shape composes with them.

Not a fit if…

You want struct-tag validation, translated / i18n messages, or a large catalogue of built-in checks (credit cards, ISO codes, CIDRs, …). Use go-playground/validator for that. This package deliberately stays small.

License

MIT. See LICENSE.md.