validate

package module
v0.0.0-...-164dcbf Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 7 Imported by: 0

README

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.

Documentation

Overview

Package validate provides small, composable value validators.

A validator is just a func(T) error: it returns nil when the value is acceptable, or an error describing the problem. Nothing has to implement an interface, so a plain closure works anywhere a validator is expected. Combine validators with All (stop at the first failure) or Collect (run them all and join every failure).

username := validate.All(
	validate.MinLength(3),
	validate.MaxLength(16),
	validate.Chars("abcdefghijklmnopqrstuvwxyz0123456789_"),
)
if err := username(input); err != nil {
	// errors.Is(err, validate.ErrDisallowedChars) still works here
}

Errors are plain sentinel values (see Error); match them with errors.Is. Err matches any error produced by this package.

Comparison with other packages

  • go-playground/validator drives validation from struct tags and reflection.
  • go-ozzo/ozzo-validation composes rules in code, but through a Rule interface over interface{}.
  • asaskevich/govalidator is a bag of IsX string helpers.

This package is the code-composition style with two constraints kept deliberately: validators are ordinary generic functions, checked by the compiler with no reflection (In, Equal and the rest will not accept the wrong type), and there are no dependencies outside the standard library.

When to use something else

Reach for go-playground/validator if you want struct-tag validation, translated messages, or a large catalogue of built-in checks. This package stays small and leaves struct/field wiring and error presentation to the caller.

Example

A validator is any func(T) error. Compose the built-ins with your own closures using All (stop at the first failure) or Collect (report all).

package main

import (
	"errors"
	"fmt"
	"strings"

	"code.aneur.in/go/validate"
)

func main() {
	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
		},
	)

	fmt.Println(username("ok_name"))
	fmt.Println(username("_nope"))
	fmt.Println(username("Nope"))
}
Output:
<nil>
must not start with an underscore
contains disallowed characters
Example (CollectAll)

Collect runs every validator and joins the failures; errors.Is still matches each one.

package main

import (
	"errors"
	"fmt"

	"code.aneur.in/go/validate"
)

func main() {
	password := validate.Collect(
		validate.MinLength(8),
		validate.Chars("abcdefghijklmnopqrstuvwxyz"),
	)

	err := password("Ab1")
	fmt.Println(err)
	fmt.Println(errors.Is(err, validate.ErrMustBeLonger))
	fmt.Println(errors.Is(err, validate.ErrDisallowedChars))
}
Output:
must contain at least 8 characters
contains disallowed characters
true
true
Example (ErrorsIs)

Errors are plain sentinels, so errors.Is works with no library-specific machinery. Err matches any error from this package.

package main

import (
	"errors"
	"fmt"

	"code.aneur.in/go/validate"
)

func main() {
	err := validate.Email("not-an-email")

	fmt.Println(err)
	fmt.Println(errors.Is(err, validate.ErrInvalidEmail))
	fmt.Println(errors.Is(err, validate.Err))
}
Output:
invalid email address
true
true

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrMustBeLonger       = NewError("must contain at least %d characters")
	ErrMustBeShorter      = NewError("must contain no more than %d characters")
	ErrMustHaveMoreBytes  = NewError("must have at least %d bytes")
	ErrMustHaveFewerBytes = NewError("must have no more than %d bytes")
)
View Source
var (
	ErrMustBeGreater        = NewError("must be greater than %v")
	ErrMustBeGreaterOrEqual = NewError("must be greater than or equal to %v")
	ErrMustBeLess           = NewError("must be less than %v")
	ErrMustBeLessOrEqual    = NewError("must be less than or equal to %v")
)
View Source
var (
	ErrMustHaveMoreItems  = NewError("must have at least %d items")
	ErrMustHaveFewerItems = NewError("must have no more than %d items")
)
View Source
var (
	ErrMissingPrefix    = NewError("must start with %q")
	ErrMissingSuffix    = NewError("must end with %q")
	ErrMissingSubstring = NewError("must contain %q")
)
View Source
var Err = Error{}

Err is the zero Error. It carries no message, so errors.Is(x, Err) is true for any error produced by this package.

View Source
var (
	ErrDisallowedChars = NewError("contains disallowed characters")
)
View Source
var (
	ErrInvalidEmail = NewError("invalid email address")
)
View Source
var (
	ErrInvalidUUID = NewError("invalid UUID")
)
View Source
var (
	ErrNoMatch = NewError("invalid format")
)
View Source
var (
	ErrNotEqual = NewError("must be equal to %v")
)
View Source
var (
	ErrRequired = NewError("is required")
)
View Source
var (
	ErrValueNotAllowed = NewError("not allowed")
)

Functions

func All

func All[T any](fs ...func(T) error) func(T) error

All validates a value against a sequence of validation functions, stopping and returning the first error. See Collect to run every function and report all failures at once.

Example
testAll := All(MinLength(4), Chars("0123456789abcdef"))
fmt.Println(testAll("invalid input"))
Output:
contains disallowed characters

func Chars

func Chars(allow string) func(string) error

Chars validates whether a string contains only allowed characters.

Example
testChars := Chars("0123456789abcdef")
fmt.Println(testChars("invalid input"))
Output:
contains disallowed characters

func Collect

func Collect[T any](fs ...func(T) error) func(T) error

Collect validates a value against a sequence of validation functions, like All, but runs every function instead of stopping at the first failure. All failures are returned together as one joined error (see errors.Join); errors.Is still matches each individual error contained in it. If nothing fails, Collect returns nil.

Example
v := Collect(MinLength(4), Chars("0123456789abcdef"))
fmt.Println(v("xy"))
Output:
must contain at least 4 characters
contains disallowed characters

func Contains

func Contains(substr string) func(string) error

Contains validates that a string contains a given substring.

func Email

func Email(value string) error

Email validates an email address using net/mail.ParseAddress.

Only a bare address is accepted (alice@example.com). Anything with a display name, angle brackets, a comment, or trailing content is rejected, as is a list of addresses. Note that ParseAddress does not require the domain to have a dot, so "alice@localhost" is considered valid; layer on Match or a DNS check if you need to be stricter.

Example
fmt.Println(Email("not an email"))
Output:
invalid email address

func Equal

func Equal[T comparable](cmp T) func(T) error

Equal validates whether an input value is equal to a comparison value.

func ExceptChars

func ExceptChars(disallow string) func(string) error

ExceptChars validates that a string contains none of the given characters.

Example
testExceptChars := ExceptChars("0123456789abcdef")
fmt.Println(testExceptChars("invalid input"))
Output:
contains disallowed characters

func In

func In[T comparable](allow ...T) func(T) error

In validates that a value equals one of the allowed values.

Example
testIn := In("abc", "def", "xyz")
fmt.Println(testIn("123"))
Output:
not allowed

func Match

func Match(re *regexp.Regexp) func(string) error

Match validates that a string matches a regular expression. Compile the expression once with regexp.MustCompile and reuse the returned validator.

Example
slug := Match(regexp.MustCompile(`^[a-z0-9-]+$`))
fmt.Println(slug("Not A Slug"))
Output:
invalid format

func Max

func Max(n int, exclusive bool) func(int) error

Max validates whether an integer is less than or equal to a given maximum. If exclusive is true, an equal value also produces an error.

Example
testMax := Max(10, true)
fmt.Println(testMax(10))
Output:
must be less than 10

func MaxFloat32

func MaxFloat32(n float32, exclusive bool) func(float32) error

MaxFloat32 validates whether a float32 is less than or equal to a given maximum. If exclusive is true, an equal value also produces an error.

func MaxFloat64

func MaxFloat64(n float64, exclusive bool) func(float64) error

MaxFloat64 validates whether a float64 is less than or equal to a given maximum. If exclusive is true, an equal value also produces an error.

func MaxLength

func MaxLength(l int) func(string) error

MaxLength validates that a string is no longer than a given maximum. Length is counted in runes, so multi-byte characters count as one.

Example
testMaxLength := MaxLength(8)
fmt.Println(testMaxLength("this string is too long"))
Output:
must contain no more than 8 characters

func MaxLengthBytes

func MaxLengthBytes(l int) func(string) error

MaxLengthBytes validates that a string is no longer than a given maximum number of bytes (len). Prefer MaxLength for a limit on visible characters; use this when the budget is genuinely a byte count, such as a fixed-width column or a wire-format field.

Example
testMaxLengthBytes := MaxLengthBytes(8)
fmt.Println(testMaxLengthBytes("cafés round the world"))
Output:
must have no more than 8 bytes

func MaxSize

func MaxSize[T any](l int) func([]T) error

MaxSize validates the length of a slice as being less than or equal to a given maximum.

Example
testMaxSize := MaxSize[string](3)
fmt.Println(testMaxSize([]string{"abc", "def", "ghi", "jkl"}))
Output:
must have no more than 3 items

func Min

func Min(n int, exclusive bool) func(int) error

Min validates whether an integer is greater than or equal to a given minimum. If exclusive is true, an equal value also produces an error.

Example
testMin := Min(10, false)
fmt.Println(testMin(5))
Output:
must be greater than or equal to 10

func MinFloat32

func MinFloat32(n float32, exclusive bool) func(float32) error

MinFloat32 validates whether a float32 is greater than or equal to a given minimum. If exclusive is true, an equal value also produces an error.

func MinFloat64

func MinFloat64(n float64, exclusive bool) func(float64) error

MinFloat64 validates whether a float64 is greater than or equal to a given minimum. If exclusive is true, an equal value also produces an error.

func MinLength

func MinLength(l int) func(string) error

MinLength validates that a string is at least a given minimum length. Length is counted in runes, so multi-byte characters count as one.

Example
testMinLength := MinLength(8)
fmt.Println(testMinLength("2short"))
Output:
must contain at least 8 characters

func MinLengthBytes

func MinLengthBytes(l int) func(string) error

MinLengthBytes validates that a string is at least a given minimum number of bytes (len). See MaxLengthBytes on when to prefer this over MinLength.

Example
testMinLengthBytes := MinLengthBytes(8)
fmt.Println(testMinLengthBytes("2short"))
Output:
must have at least 8 bytes

func MinSize

func MinSize[T any](l int) func([]T) error

MinSize validates the length of a slice as being greater than or equal to a given minimum.

Example
testMaxSize := MinSize[string](3)
fmt.Println(testMaxSize([]string{"abc", "def"}))
Output:
must have at least 3 items

func NotIn

func NotIn[T comparable](disallow ...T) func(T) error

NotIn validates that a value equals none of the disallowed values.

func Prefix

func Prefix(prefix string) func(string) error

Prefix validates that a string begins with a given prefix.

Example
fmt.Println(Prefix("https://")("http://example.com"))
Output:
must start with "https://"

func Required

func Required[T comparable]() func(T) error

Required validates that a value is not the zero value for its type: an empty string, 0, a nil pointer, and so on.

It is defined for comparable types only. For slices and maps, use MinSize(1); for "non-whitespace", combine with a trimming step.

Example
notBlank := Required[string]()
fmt.Println(notBlank(""))
Output:
is required

func Suffix

func Suffix(suffix string) func(string) error

Suffix validates that a string ends with a given suffix.

func URL

func URL(value string) error

URL validates that a string is an absolute URL (with a scheme) or an absolute path, per net/url.ParseRequestURI. "example.com" with no scheme is rejected.

Example
fmt.Println(URL("not a url"))
Output:
invalid URL

func UUID

func UUID(value string) error

UUID validates a UUID string in the canonical 8-4-4-4-12 hyphenated form. Only lowercase hexadecimal is accepted; the version and variant bits are not checked.

Example
fmt.Println(UUID("not a uuid"))
Output:
invalid UUID

Types

type Error

type Error struct {
	Message string
	Data    []any
}

Error represents a validation error.

var (
	ErrInvalidURL Error = NewError("invalid URL")
)

func NewError

func NewError(message string) Error

NewError creates a new validation error.

func (Error) Error

func (e Error) Error() string

Error returns the error message. If Data is non-empty, Message is used as an fmt.Sprintf format string and Data as its arguments (this is how the sentinels with %d/%q verbs are filled in, e.g. via With).

func (Error) Is

func (e Error) Is(target error) bool

Is determines whether the Error is an instance of the target. https://pkg.go.dev/errors#Is

If the target is a validation error and specifies a message, this function returns true if the messages match. If the target is an empty validation error, this function always returns true.

func (Error) With

func (e Error) With(value any) Error

With returns a copy of the Error with value appended to Data, so it lands in the message when Message contains a formatting verb:

ErrMustBeLonger.With(4) // "must contain at least 4 characters"

Jump to

Keyboard shortcuts

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