validator

package module
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 28 Imported by: 0

README

validator

Doc Go Release Test Report Card Stars License

A modern, zero-dependency, high-performance Go Struct and Field validator with a boolean rule DSL.

Features

  • Zero third-party dependencies — only the Go standard library.
  • Boolean DSL&&, ||, !, (), precedence ! > && > ||.
  • Compile-once, run-many — expressions compile to cached closures.
  • 6 entry pointsStruct, Map, JSON, URLValues, Any, Var.
  • And more — cross-field, diving, bind/SafeBind, context, optional concurrency, i18n.

Requires Go 1.25+.

Install

go get github.com/libtnb/validator

Quick start

import (
    "context"
    "fmt"

    "github.com/libtnb/validator"
)

type User struct {
    Email string `validate:"required && email"`
    Pwd   string `validate:"required && min:8"`
    Pwd2  string `validate:"required && same:Pwd"`   // cross-field
    Tags  []string `validate:"dive && alpha"`        // each element
    Role  string `validate:"required && in:admin,user,guest"`
}

user := User{Email: "a@b.com", Pwd: "secret12", Pwd2: "secret12", Role: "user"}

vd := validator.Struct(user)
vd.Validate(context.Background())
if vd.Fails() {
    fmt.Println(vd.Errors().All()) // map[field]map[rule]message
}

var out User
vd.SafeBind(&out) // bind the validated, filtered values into out

// just pass/fail? Valid takes the allocation-free fast path (0 B/op)
if validator.Valid(user) { /* ... */ }

Maps / JSON / form values, with explicit rules:

vd := validator.Map(
    map[string]any{"email": "a@b.com", "age": 30},
    map[string]string{
        "email": "required && email",
        "age":   "required && (gte:18 && lte:120)",
    },
)
vd.Validate(context.Background())

validator.JSON(`{"email":"a@b.com"}`, rules)           // decodes then validates
validator.URLValues(r.Form, rules)                     // form data
validator.Var("a@b.com", "required && email")          // a single value

The DSL

Construct Meaning
a && b both must pass
a || b at least one must pass
!a must NOT pass
(a || b) && c grouping; precedence is ! > && > ||
rule:arg1,arg2 rule with arguments (in:a,b,c, min:3)
regex:"^(a|b)+$" quoted argument: | & ( ) are literal inside quotes
dive apply the following rules to each slice/array/map element

Argument quoting. Operators are the double-character forms && / ||, so a single | inside a regex is never mistaken for OR. For arguments containing | & ( ), wrap them in quotes (regex:"...", only \" and \\ are escapes inside quotes) or escape with a backslash in bare form (\| \& \( \)). Regex metacharacters like \d are preserved.

Semantics

Empty values (omitempty). Non-presence rules pass on empty/zero values, so an optional field is only checked when present. This is why ! differs from not_*: in:a,b passes on "" (omitempty), so !in:a,b rejects "" while not_in:a,b passes it. Negate a non-presence rule with its not_*/ne form, not ! (which is for presence rules like !required).

required. By default required asserts present and non-nil: a Go zero value ("", 0, false) counts as provided and passes, while a nil pointer or an absent map/JSON key fails. To reject empty values, use notblank/filled for strings, or WithStrictRequired() to make required reject zero values too.

Sizes: length vs value. min/max/between/gt/gte/lt/lte/len/size compare numbers by value and strings by rune count; add numeric to compare a string by value:

"age": "numeric && gte:18"   // "30" compares as the number 30
"pwd": "required && min:8"   // "999" is 3 characters, fails

Built-in rules

Args are the rule:arg values ( = none; = repeatable; ? = optional).

Presence

Rule Args Passes when
required present and non-nil (non-zero under WithStrictRequired)
filled not empty
notblank has a non-whitespace character
sometimes marker: when the field is absent (missing key, nil pointer), skip every rule on the field — "sometimes && required && email" gives PATCH semantics
required_if field,val… required when field equals any listed value
required_unless field,val… required unless field equals any listed value
required_with field… required when any listed field is present
required_without field… required when any listed field is missing
required_with_all field… required when all listed fields are present
required_without_all field… required when all listed fields are missing
excluded_if field,val… must be empty when field equals any listed value
excluded_unless field,val… must be empty unless field equals any listed value
excluded_with field… must be empty when any listed field is present
excluded_without field… must be empty when any listed field is missing

String

Rule Args Passes when
alpha letters only
alphanum letters and digits only
ascii ASCII characters only
lowercase all lowercase
uppercase all uppercase
contains sub contains the substring
excludes sub does not contain the substring
startswith prefix has the prefix
endswith suffix has the suffix

Format

Rule Args Passes when
email a valid email address
url / uri a valid URL / URI
uuid / ulid a valid UUID / ULID
ip / ipv4 / ipv6 a valid IP / IPv4 / IPv6 address
cidr / cidrv4 / cidrv6 valid CIDR notation (any / IPv4 / IPv6)
mac a valid MAC address
hostname / fqdn a valid RFC 1123 hostname / FQDN (TLD required)
port an integer port in [1, 65535]
e164 an E.164 phone number (+14155552671)
json valid JSON
base64 a valid base64 string
jwt a three-segment JWT shape
semver a semantic version (1.2.3-rc.1)
hexcolor a hex color (#fff, #ffaa00cc)
latitude / longitude a decimal coordinate in range
timezone an IANA time zone name (Asia/Shanghai)
luhn digits passing the Luhn checksum
credit_card a 12-19 digit card number passing Luhn
datetime layout? parses with the Go time layout (default layouts if omitted)
date a valid date
regex pattern matches the pattern
not_regex pattern does not match the pattern

Time — an arg that parses as a date is always a literal bound (after:2026-01-01; input keys can never shadow it), otherwise it resolves as a sibling field name (after:Start). Values may be time.Time, date strings, or unix timestamps.

Rule Args Passes when
after / after_or_equal field|date a date after (or equal to) the reference
before / before_or_equal field|date a date before (or equal to) the reference

File (*multipart.FileHeader fields; ext also accepts filename strings)

Rule Args Passes when
ext jpg,png,… the filename has one of the extensions (case-insensitive)
mimetypes type… the sniffed content (first 512 bytes, not the client header) matches; image/* wildcards work
filemin / filemax size file size ≥ / ≤ size (512kb, 10mb, 1.5gb; 1024-based)

Content starting with markup (< + letter/!///?) is never sniffed as text/plain<svg onload=…> and unlisted tags cannot pass a plain-text allowlist (stricter than http.DetectContentType). Literal < in prose (a < b, <3) stays plain text.

Numeric / sizesize = value for numbers, rune length for strings (see above); n is a number.

Rule Args Passes when
min / max n size ≥ n / ≤ n
between min,max size within [min, max]
gt / gte n size > n / ≥ n
lt / lte n size < n / ≤ n
len / size n size == n
digits n exactly n digits
numeric a number or numeric string
number an integer
boolean convertible to a boolean

Comparison

Rule Args Passes when
in / in_ci a,b,… equals one of the values (_ci = case-insensitive)
not_in a,b,… equals none of the values
eq / ne v equals / does not equal v
eq_ignore_case / ne_ignore_case v case-insensitive equals / not equals
unique a slice/array has no duplicate elements (a map no duplicate values)

Cross-field

Rule Args Passes when
same / eqfield field equals another field
different / nefield field differs from another field
gtfield / gtefield field > / ≥ another field (numbers, or time.Time pairs chronologically)
ltfield / ltefield field < / ≤ another field (numbers, or time.Time pairs chronologically)
confirmed <field>_confirmation exists and matches

Filters (transform the value before validation / SafeBind)

Filter Args Effect
trim / ltrim / rtrim strip surrounding / leading / trailing whitespace
lower / upper / title lower-case / upper-case / title-case
int / float / bool / string convert to that type

The is subpackage exposes the format checks as plain functions (is.Email, is.URL, ...) for reuse outside the validator.

Custom rules

Field.Val() returns a reflect.Value (the value flows without boxing, so the validation hot path is allocation-free). Read it with the reflect.Value getters, or f.Val().Interface() to get the value as any for the conv helpers.

v := validator.NewValidator()

// string style — omitempty and string rendering are pre-applied,
// fn holds only the actual check
v.RegisterStringFunc("slug", func(s string, args ...string) bool {
    return !strings.Contains(s, " ")
}, "The {field} must be a slug.")

// function style — read the value via reflect.Value (0-alloc)
v.RegisterFunc("even", func(f validator.Field) bool {
    rv := f.Val()
    return rv.CanInt() && rv.Int()%2 == 0
}, "The {field} must be even.")

// interface style: implement Signature() / Passes(Field) bool / Message()
v.RegisterRule(&MyRule{})

Rules that need a context.Context or return an error (e.g. a DB uniqueness check) implement validator.ErrorRule instead and read f.Context().

Structured errors & tag linting

vd.Errors().Items()             // []FieldError{Field, Rule, Message, Params} — build API error payloads
es, ok := validator.AsErrors(err) // recover the collection from a (wrapped) Validation.Err()

// catch tag typos in a test instead of at request time:
// unknown rules, DSL syntax errors and bad static args, all fields at once
func TestRequestTags(t *testing.T) {
    if err := validator.CheckRules(CreateUserRequest{}); err != nil {
        t.Fatal(err)
    }
}

Messages, attributes & i18n

v := validator.NewValidator(
    validator.WithAttributes(map[string]string{"email": "Email address"}),
    validator.WithMessages(map[string]string{
        "email.required": "Please provide your email.", // field-level
        "required":       "This field is required.",     // rule-level
    }),
    validator.WithTranslation(translations.ZhHans()),    // i18n fallback
)

Locale packs (ZhHans, ZhHant, Ja, Ko, Es) live in the github.com/libtnb/validator/translations subpackage.

Templates use {field} (replaced by the attribute alias or field name) and {0}, {1}, ... (the rule arguments). Validation.AddMessages overrides templates for a single run:

vd := v.Struct(req)
vd.AddMessages(map[string]string{"email.required": "We need your email."})

Priority: AddMessages > WithMessages > i18n > built-in English; within a map, field.rule beats rule.

The package-level default

The top-level helpers (validator.Struct, Map, JSON, ...) run on a shared default instance. SetDefault replaces it process-wide — install a configured validator once at startup (like slog.SetDefault) and the rest of the program can validate through the package funcs without passing a *Validator around:

v := validator.NewValidator(validator.WithTranslation(translations.ZhHans()))
v.RegisterErrorRule(myDBRule)
validator.SetDefault(v)

// anywhere else
vd := validator.Struct(req) // uses the installed default

Default() returns the current instance; before any SetDefault it is a lazily-created plain NewValidator().

Contrib modules

Database-backed rules live in separate zero-impact modules so the core stays dependency-free:

go get github.com/libtnb/validator/contrib/gormrules

gormrules.Register(v, db) adds exists:table,col[,col...] and not_exists:table,col[,col...] backed by a *gorm.DB, with SQL-identifier validation on the rule arguments.

License

See LICENSE.

Documentation

Overview

Package validator is a fast Go validator whose headline is a boolean rule DSL combining rules with &&, ||, !, () at precedence ! > && > || (e.g. `required && (in:admin,user || regex:"^g.+")`).

Data sources: struct tags, map, JSON, url.Values, Any, or a single Var.

required means present-and-non-nil: a zero value ("", 0, false) passes, a nil pointer or absent key fails. Use notblank/filled to reject empty strings, or WithStrictRequired to demand a non-zero value.

To negate a non-presence rule use its not_*/ne form, not !: omitempty makes in:a,b pass on "", so !in:a,b rejects "" while not_in:a,b passes it. ! is for presence rules like !required.

Example
package main

import (
	"context"
	"fmt"

	"github.com/libtnb/validator"
)

func main() {
	type User struct {
		Email string `validate:"required && email"`
		Age   int    `validate:"required && gte:18"`
	}
	vd := validator.Struct(User{Email: "a@b.com", Age: 20})
	vd.Validate(context.Background())
	fmt.Println("valid:", !vd.Fails())
}
Output:
valid: true

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func CheckRules added in v0.2.0

func CheckRules(data any) error

CheckRules reports every bad rule tag on data's struct type; see Validator.CheckRules.

func IsEmpty

func IsEmpty(v any) bool

IsEmpty reports the zero/empty value for omitempty semantics.

func IsEmptyValue

func IsEmptyValue(rv reflect.Value) bool

IsEmptyValue is the reflect.Value form of IsEmpty.

func SetDefault added in v0.3.0

func SetDefault(v *Validator)

SetDefault makes v the Validator returned by Default and used by the package-level helpers (Struct, Map, JSON, ...). Like slog.SetDefault it is meant to be called once during startup, before concurrent use — typically with an instance carrying custom rules, messages and translations, so the rest of the program can validate through the package funcs without passing a *Validator around.

func Valid

func Valid(data any) bool

Valid reports whether data passes its struct-tag rules on the allocation-free fast path; data carrying no rules (a plain map or scalar) is vacuously valid. Use Struct when you need error details, Bind, or to add rules.

Types

type ErrorRule

type ErrorRule interface {
	Signature() string
	PassesE(f Field) error
	Message() string
}

ErrorRule is a Rule variant returning an error; the engine prefers PassesE over Passes.

func ErrorRules

func ErrorRules() []ErrorRule

type Errors

type Errors interface {
	// One returns the first message overall.
	One() string
	// OneFor returns the first message for field.
	OneFor(field string) string
	// Messages returns field's rule->message map.
	Messages(field string) map[string]string
	All() map[string]map[string]string
	// Items returns every failure in evaluation order with resolved messages.
	Items() []FieldError
	Has(field string) bool
	// String aggregates all messages (fields sorted), or "" when none failed.
	String() string
}

Errors reads validation failures. Pure collection; deliberately not an error (use Validation.Err() for nil-on-success).

func AsErrors added in v0.2.0

func AsErrors(err error) (Errors, bool)

AsErrors extracts the Errors collection from an error returned by Validation.Err (works through wrapping via errors.As).

type Field

type Field interface {
	// Val is the current value; absent/nil is an invalid Value.
	Val() reflect.Value
	// Attrs are the parsed rule arguments (in:a,b,c -> [a b c]). The slice is
	// shared with the compiled expression cache: read-only, never modify it.
	Attrs() []string
	// Name is the error-reporting name; for a dive element it is the bracketed
	// key ("tags[0]") on the diagnostic path.
	Name() string
	// RootData is the whole data set, not this field's value.
	RootData() any
	Context() context.Context
	// Sibling resolves another field (dotted name) relative-first then root-anchored, never itself.
	Sibling(name string) (Field, bool)
}

Field is a value being validated, exposing sibling fields for cross-field rules. A Field is only valid for the duration of the rule call that receives it (the engine pools and reuses instances): never retain one.

type FieldError

type FieldError struct {
	Field string
	Rule  string
	// Message is the raw template ({field}/{0}/...); root resolves it.
	Message string
	// Params are the rule arguments, resolving {0},{1},... placeholders.
	Params []string
}

FieldError is the flat unit of a validation failure.

type FieldRules added in v0.4.0

type FieldRules struct {
	// Name is the field's validation name, dotted for nested fields.
	Name string
	// Index locates the field for reflect.Type.FieldByIndex.
	Index []int
	// Rules apply to the field value itself.
	Rules []RuleInfo
	// Element rules apply to slice/array/map elements (dive).
	Element []RuleInfo
	// Exact is false when || or ! branches were omitted.
	Exact bool
}

FieldRules lists the rules declared on one struct field, flattened from the top-level AND chain of its expression. Element holds the rules that apply to container elements (after dive). Rules under || or ! cannot be flattened losslessly: those subtrees are omitted and Exact reports false.

func DescribeRules added in v0.4.0

func DescribeRules(data any) ([]FieldRules, error)

DescribeRules calls Validator.DescribeRules on the package-level default.

type Filter

type Filter interface {
	Signature() string
	Handle(val any, args ...string) (any, error)
}

Filter transforms or sanitizes an input value (trim, lower, int, ...).

func Filters

func Filters() []Filter

type Option

type Option func(*Validator)

Option configures a Validator during initialization.

func WithAttributes

func WithAttributes(attrs map[string]string) Option

WithAttributes maps field names to display names for {field}.

func WithMessages

func WithMessages(messages map[string]string) Option

WithMessages sets message templates keyed by "field.rule" (highest priority) or "rule".

func WithParallel

func WithParallel(minFields int) Option

WithParallel validates fields concurrently once a validation has at least minFields; values < 1 disable parallelism.

func WithPrivateFieldValidation

func WithPrivateFieldValidation() Option

WithPrivateFieldValidation enables validation of unexported fields (via unsafe).

func WithStrictRequired

func WithStrictRequired() Option

WithStrictRequired makes `required` reject zero values, not just absent/nil.

func WithTagName

func WithTagName(name string) Option

WithTagName sets the struct tag used for validation rules (default "validate").

func WithTagNameFunc

func WithTagNameFunc(fn TagNameFunc) Option

WithTagNameFunc derives the tag name per field.

func WithTransformFunc

func WithTransformFunc(fn TransformFunc) Option

WithTransformFunc sets a final transform over each resolved message.

func WithTranslation

func WithTranslation(messages map[string]string) Option

WithTranslation sets localized message templates keyed by "field.rule" or "rule".

func WithTranslator

func WithTranslator(fn TranslatorFunc) Option

WithTranslator sets a function consulted after WithTranslation's map.

func WithoutBuiltinRules

func WithoutBuiltinRules() Option

WithoutBuiltinRules disables registration of the built-in rules and filters.

type Rule

type Rule interface {
	Signature() string
	Passes(f Field) bool
	Message() string
}

Rule is a leaf boolean rule; composition (&&, ||, !) is handled by the DSL. Passes must be deterministic and side-effect free: the engine may evaluate a rule more than once for one value (fast probe + diagnostics on dive elements, exhaustive Errors() collection).

func Rules

func Rules() []Rule

Rules returns a copy of the catalog; the copy guards the shared backing slice.

type RuleInfo added in v0.4.0

type RuleInfo struct {
	Name string
	Args []string
}

RuleInfo is one rule invocation in a field's expression, e.g. min:3.

type TagNameFunc

type TagNameFunc func(field reflect.StructField) string

TagNameFunc derives a field's validation name from its StructField; an empty return keeps the Go field name.

type TransformFunc

type TransformFunc func(message string) string

TransformFunc is a final transform applied to each resolved error message.

type TranslatorFunc

type TranslatorFunc func(rule string) (string, bool)

TranslatorFunc returns a rule's template, or false to fall through.

type Validation

type Validation interface {
	// AddRules ANDs rule expressions onto a field's existing expression.
	AddRules(field string, rules ...string) error
	AddFilters(field string, filters ...string) error
	RemoveRules(field string, rules ...string) error
	RemoveFilters(field string, filters ...string) error
	// ClearRules/ClearFilters drop a field's whole expression/chain.
	ClearRules(field string) error
	ClearFilters(field string) error
	// AddMessages overrides message templates for this run only, outranking WithMessages.
	AddMessages(messages map[string]string) error
	Rules() map[string]string
	Filters() map[string]string
	// Bind writes the ORIGINAL data to ptr without requiring Validate.
	Bind(ptr any) error
	// SafeBind writes the FILTERED data to ptr; errors unless Validate passed.
	SafeBind(ptr any) error
	// Validate must run before accessing errors.
	Validate(ctx context.Context)
	// Errors is ALWAYS non-nil and not an error; never compare it to nil.
	Errors() Errors
	// Err is nil on success, else the failures.
	Err() error
	Fails() bool
}

Validation represents a single validation run over one data set. It is not safe for concurrent use; configure it (AddRules/AddFilters) before Validate — mutators after Validate have no effect on the already-computed result.

func Any

func Any(data any) Validation

func JSON

func JSON(data string, rules map[string]string) Validation

func Map

func Map(data map[string]any, rules map[string]string) Validation

func Struct

func Struct(data any) Validation

func URLValues

func URLValues(data url.Values, rules map[string]string) Validation

func Var

func Var(value any, rule string) Validation
Example
package main

import (
	"context"
	"fmt"

	"github.com/libtnb/validator"
)

func main() {
	vd := validator.Var("nope", "required && email")
	vd.Validate(context.Background())
	fmt.Println(vd.Fails())
}
Output:
true

type Validator

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

Validator is a reusable, concurrency-safe factory for validations.

func Default

func Default() *Validator

Default returns the shared Validator behind the package funcs; mutating it is process-global. It is created on first use unless SetDefault installed one earlier.

func NewValidator

func NewValidator(options ...Option) *Validator

NewValidator creates a Validator, registering built-in rules unless WithoutBuiltinRules is given.

func (*Validator) Any

func (v *Validator) Any(data any) Validation

Any validates a map, a struct (tags are read), or a scalar.

func (*Validator) CheckRules added in v0.2.0

func (v *Validator) CheckRules(data any) error

CheckRules eagerly compiles every rule expression reachable from data's struct type (nested and embedded fields included) and reports all bad tags — unknown rules, DSL syntax errors, bad static args — as one joined error. Call it from a test or at startup to catch tag typos before request time.

func (*Validator) DescribeRules added in v0.4.0

func (v *Validator) DescribeRules(data any) ([]FieldRules, error)

DescribeRules reports the validation rules declared on data's struct type, for consumers that translate them into another representation — an OpenAPI generator mapping min:3 to minLength, for instance. Unknown rules are reported as-is rather than rejected; pair with CheckRules to catch typos.

func (*Validator) JSON

func (v *Validator) JSON(data string, rules map[string]string) Validation

JSON decodes and validates a JSON object; a decode error or non-object top-level value is reported as a validation error.

func (*Validator) Map

func (v *Validator) Map(data map[string]any, rules map[string]string) Validation

Map validates a map against the given field->rule expressions.

Example
package main

import (
	"context"
	"fmt"

	"github.com/libtnb/validator"
)

func main() {
	v := validator.NewValidator()
	vd := v.Map(
		map[string]any{"name": "alice"},
		// boolean DSL: AND-grouped OR
		map[string]string{"name": "required && (alpha || in:bob,carol)"},
	)
	vd.Validate(context.Background())
	fmt.Println("valid:", !vd.Fails())
}
Output:
valid: true

func (*Validator) RegisterErrorRule

func (v *Validator) RegisterErrorRule(rule ErrorRule)

func (*Validator) RegisterFilter

func (v *Validator) RegisterFilter(f Filter)

func (*Validator) RegisterFunc

func (v *Validator) RegisterFunc(signature string, fn func(Field) bool, message string)

RegisterFunc registers a rule from a plain function.

func (*Validator) RegisterRule

func (v *Validator) RegisterRule(rule Rule)

RegisterRule registers a custom rule and invalidates caches so later validations pick it up; register before constructing the validations that use it. An unusable signature (blank, DSL syntax, reserved "dive") panics, like http.ServeMux.Handle.

func (*Validator) RegisterStringFunc added in v0.2.0

func (v *Validator) RegisterStringFunc(signature string, fn func(value string, args ...string) bool, message string)

RegisterStringFunc registers a string rule with the built-in conventions pre-applied: empty values pass (omitempty) and the value arrives rendered as a string, so fn only holds the actual check.

func (*Validator) Struct

func (v *Validator) Struct(data any) Validation

Struct validates a struct using its tags plus any added rules.

func (*Validator) URLValues

func (v *Validator) URLValues(data url.Values, rules map[string]string) Validation

URLValues validates form data, using the first value of each key.

func (*Validator) Valid

func (v *Validator) Valid(data any) bool

Valid reports whether data passes its struct-tag rules on the allocation-free fast path; data carrying no rules is vacuously valid. Use Struct when you need error details, Bind, or to add rules.

func (*Validator) Var

func (v *Validator) Var(value any, rule string) Validation

Var validates a single value; the field is named "value" and cross-field rules are not meaningful.

Directories

Path Synopsis
Command examples is a runnable validator demo; the leading-underscore dir is skipped by the go tool, so run it directly: go run ./_examples
Command examples is a runnable validator demo; the leading-underscore dir is skipped by the go tool, so run it directly: go run ./_examples
contrib
gormrules module
openapi module
Package conv provides fast type conversions.
Package conv provides fast type conversions.
internal
dsl
Package dsl is the boolean rule-expression engine: lexer, parser (! > && > ||), AST, dive splitting.
Package dsl is the boolean rule-expression engine: lexer, parser (! > && > ||), AST, dive splitting.
Package is provides pure predicates for common string formats.
Package is provides pure predicates for common string formats.

Jump to

Keyboard shortcuts

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