Composable validation for Go with fluent builders, rule tags, struct validation, structured errors, safe regex handling, caching, universal domain format validators, custom rules, and optional message translation.
github.com/aatuh/validate/v3: main API (New, builders,FromTag, struct validation)github.com/aatuh/validate/v3/core: cache-aware validation enginegithub.com/aatuh/validate/v3/glue: builder and engine integrationgithub.com/aatuh/validate/v3/types: rule AST, tag parser, compiler, and extension hooksgithub.com/aatuh/validate/v3/errors: structured errors and stable codesgithub.com/aatuh/validate/v3/structvalidator: reflection-based struct validationgithub.com/aatuh/validate/v3/translator: message translation helpersgithub.com/aatuh/validate/v3/validators/...: root and optional plugin validators
v := validate.New()
checkName := v.String().Required().MinRunes(3).MaxRunes(40).Build()
_ = checkName("gopher")
type User struct {
Email string `validate:"string;required;email"`
Age int `validate:"int;min=18"`
}
_ = v.ValidateStruct(User{Email: "user@example.com", Age: 30})Run examples:
go test ./examples -v -count 1Tags keep the existing v3 grammar: the first token is usually the base type, followed by semicolon-separated rules. Existing tags remain supported.
Generic rules:
| Tag | Meaning |
|---|---|
| required | Value must be non-zero/non-empty |
| omitempty | Skip validation for zero, nil, empty string, empty slice, or empty map |
Custom rule tags:
| Tag | Meaning |
|---|---|
ruleName |
Apply a registered custom compiler named ruleName |
custom:ruleName |
Apply a registered custom compiler explicitly |
custom:ruleName=raw |
Apply a registered custom compiler with Args["value"] == "raw" |
Bare custom rule names and custom: rules are supported after every base type.
Malformed built-in rule arguments, such as int;min=bad, still fail during
tag parsing. Unregistered custom rules fail during compilation or validation
with code unknown.
String rules:
| Tag | Meaning |
|---|---|
| len=N or length=N | Exact byte length |
| min=N / max=N | Minimum / maximum byte length |
| minRunes=N / maxRunes=N | Minimum / maximum Unicode rune count |
| nonempty | String must not be empty |
| oneof=a,b,c | Value must be one listed value |
| regex=PATTERN | Full-match regexp; anchors are added and input length is capped |
| contains=X / notContains=X | Required/prohibited substring |
| prefix=X / suffix=X | Required prefix/suffix |
| url / hostname | Absolute URL or hostname |
| ip / ipv4 / ipv6 / cidr | IP address or CIDR prefix |
| ascii / alpha / alnum | Character class checks |
| email / uuid / ulid | Built-in string plugins imported by the root package |
| slug / semver / json / jwt | Universal zero-dependency format validators |
| base64 / base64url / hex / mac | Encoding and identifier format validators |
| e164 / fqdn / date / rfc3339 / luhn | Phone, DNS, date/time, and checksum format validators |
| uuidv1 / uuidv3 / uuidv4 / uuidv5 / uuidv6 / uuidv7 / uuidv8 | Canonical UUID with version and RFC variant checks |
Number rules:
| Type | Tags |
|---|---|
| int / int64 | min=N, max=N, gt=N, gte=N, lt=N, lte=N, between=A,B, positive, nonnegative |
| float | finite, min=N, max=N, gt=N, gte=N, lt=N, lte=N, between=A,B, positive, nonnegative |
Collection and other rules:
| Type | Tags |
|---|---|
| bool | true, false |
| slice | len=N, length=N, min=N, max=N, unique, contains=X, foreach=(...) |
| array | len=N, length=N, min=N, max=N, unique, contains=X, foreach=(...) |
| map | len=N, length=N, min=N, max=N, minKeys=N, maxKeys=N, keys=(...), values=(...) |
| time | notzero, before=RFC3339, after=RFC3339, between=RFC3339,RFC3339 |
Examples:
_ = v.CheckTag("slice;min=1;foreach=(string;minRunes=2)", []string{"go"})
_ = v.CheckTag("array;len=2;foreach=(string;slug)", [2]string{"api", "docs"})
_ = v.CheckTag("map;keys=(string;min=2);values=(int;positive)", map[string]int{"id": 1})
_ = v.CheckTag("time;after=2026-01-01T00:00:00Z", time.Now().UTC())Domain validators are conservative format checks. They do not verify ownership, deliverability, country-specific numbering plans, payment-card brands, JWT signatures, JWT claims, DNS resolution, or registry authority.
_ = v.String().SemVer().Build()("1.2.3")
_ = v.CheckTag("string;e164", "+358401234567")
jwtToken := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjMifQ.c2lnbmF0dXJl"
_ = v.CheckTag("slice;foreach=(string;jwt)", []string{jwtToken})Struct validation uses validate tags, skips unexported fields, recurses into
nested structs, slices, arrays, and maps, and returns deterministic structured
errors.
type Signup struct {
Email *string `json:"email" validate:"string;omitempty;email"`
Password string `json:"password" validate:"string;required;min=8"`
Confirm string `json:"confirm" validate:"string;eqField=Password"`
Token string `json:"token" validate:"string;requiredWith=Password"`
}
err := v.ValidateStructWithOpts(input, validate.ValidateOpts{
FieldNameFunc: validate.JSONFieldName,
})Struct-only cross-field rules:
| Tag | Meaning |
|---|---|
| eqField=FieldName | Value must equal another field on the same struct |
| neField=FieldName | Value must differ from another field on the same struct |
| requiredWith=FieldName | Value is required when the referenced field is non-zero |
| requiredIf=FieldName,value | Value is required when the referenced field equals value |
| requiredUnless=FieldName,value | Value is required unless the referenced field equals value |
| struct:ruleName or struct:ruleName=raw | Apply a registered struct-level rule to the current field |
Cross-field tags reference same-level Go field names. A missing or
inaccessible referenced field returns field.reference on the current field.
Conditional values are compared with exact string formatting and do not support
escaping commas in this version.
Existing validators are fail-fast by default. Opt in to collecting all rule
failures for a single value with CompileOpts{CollectAll: true}:
err := v.CheckTagWithOpts("string;min=5;max=2", "abc", validate.CompileOpts{
CollectAll: true,
})omitempty still skips zero values. Failed requiredness rules, including
required, requiredWith, requiredIf, and requiredUnless, short-circuit
later same-field rules with only the requiredness code.
Context-aware APIs are additive. Built-in rules check cancellation before rule execution; custom context compilers can read request-scoped context values:
err := v.CheckTagContext(ctx, "string;required", value)Validation failures return errors.Errors, a stable slice of field errors:
var es validate.Errors
if errors.As(err, &es) {
_ = es.AsMap()
_ = es.Filter("email")
}Each field error contains:
Path: deterministic field pathCode: stable machine-readable code such asstring.min,required, ormap.minkeysParam: optional simple rule parameterMsg: translated human-readable message
Prefer Code, Path, and Param for program logic. Built-in validation
messages do not echo submitted values; invalid regex pattern diagnostics use a
capped/redacted pattern preview. Map keys in Path preserve short ordinary
keys such as items[id]; long keys, sensitive-looking keys, private-looking
keys, and keys that would need escaping are rendered as [<redacted>].
Custom validators and translators control their own messages, so avoid
including secrets, tokens, or private caller data there.
validate.New() installs default English translations and root-level plugin
translations for email, UUID, and ULID. Use validate.NewWithTranslator or
WithTranslator to provide custom messages.
errors/codes.go is the source of truth for stable built-in codes. Program
logic should use codes rather than English messages.
| Code | Related rule or condition |
|---|---|
unknown |
Unknown rules, malformed struct rule tags, or non-structured errors |
required |
required |
required.with |
requiredWith |
required.if |
requiredIf |
required.unless |
requiredUnless |
omitempty |
Informational skipped empty value |
field.eq |
eqField |
field.ne |
neField |
field.reference |
Missing or inaccessible referenced struct field |
string.type |
Expected string |
string.length |
len / length |
string.min |
min byte length |
string.max |
max byte length |
string.nonempty |
nonempty |
string.pattern |
Legacy pattern code |
string.oneof |
oneof |
string.prefix |
prefix |
string.suffix |
suffix |
string.contains |
contains |
string.notContains |
notContains |
string.url |
url |
string.hostname |
hostname |
string.ip |
ip, ipv4, or ipv6 |
string.cidr |
cidr |
string.ascii |
ascii |
string.alpha |
alpha |
string.alnum |
alnum |
string.regex.invalidPattern |
Invalid regex pattern |
string.regex.inputTooLong |
Regex input length cap |
string.regex.noMatch |
Regex mismatch |
string.minRunes |
minRunes |
string.maxRunes |
maxRunes |
int.type |
Expected integer |
int64.type |
Expected exact int64 |
number.type |
Expected number |
int.min |
Integer min |
int.max |
Integer max |
number.min |
Float/number min |
number.max |
Float/number max |
number.positive |
positive |
number.nonnegative |
nonnegative |
number.between |
between |
number.gt |
gt |
number.gte |
gte |
number.lt |
lt |
number.lte |
lte |
number.finite |
finite |
float.type |
Expected float |
slice.type |
Expected slice |
slice.length |
Slice len / length |
slice.min |
Slice min |
slice.max |
Slice max |
slice.forEach |
Element validation wrapper |
slice.unique |
unique |
slice.contains |
contains |
array.type |
Expected array |
array.length |
Array len / length |
array.min |
Array min |
array.max |
Array max |
array.forEach |
Element validation wrapper |
array.unique |
unique |
array.contains |
contains |
map.type |
Expected map |
map.length |
Map len / length |
map.minkeys |
min / minKeys |
map.maxkeys |
max / maxKeys |
map.keys |
keys=(...) |
map.values |
values=(...) |
bool.type |
Expected bool |
bool.true |
true |
bool.false |
false |
time.type |
Expected time.Time |
time.notzero |
notzero |
time.before |
before |
time.after |
after |
time.between |
between |
string.slug.invalid |
slug |
string.semver.invalid |
semver |
string.json.invalid |
json |
string.jwt.invalid |
jwt |
string.base64.invalid |
base64 |
string.base64url.invalid |
base64url |
string.hex.invalid |
hex |
string.mac.invalid |
mac |
string.e164.invalid |
e164 |
string.fqdn.invalid |
fqdn |
string.date.invalid |
date |
string.rfc3339.invalid |
rfc3339 |
string.luhn.invalid |
luhn |
string.uuid.version |
uuidv1, uuidv3, uuidv4, uuidv5, uuidv6, uuidv7, or uuidv8 version/variant mismatch |
Per-instance rule compilers work from tags, manual rules, and builder escape hatches:
v := validate.New().
WithRuleCompiler("even", func(c *types.Compiler, rule types.Rule) (func(any) error, error) {
return func(value any) error {
n, ok := value.(int)
if !ok || n%2 != 0 {
return validate.Errors{{Code: "number.even", Msg: c.T("number.even", "must be even", nil)}}
}
return nil
}, nil
}).
WithRuleCompiler("mod", func(c *types.Compiler, rule types.Rule) (func(any) error, error) {
raw, _ := rule.Args["value"].(string)
return func(value any) error {
// parse raw and validate value
_ = raw
return nil
}, nil
})
_ = v.CheckTag("int;even", 2)
_ = v.CheckTag("int;custom:mod=2", 4)
_ = v.Int().Rule("even", nil).Build()(2)Use WithContextRuleCompiler when a custom rule must observe cancellation or
request-scoped context values. Existing WithRuleCompiler rules continue to
work through context-aware APIs by ignoring the context.
Compile-error-aware callers can use CompileRulesE:
check, err := v.CompileRulesE([]validate.Rule{
validate.NewRule(validate.KInt, nil),
validate.NewRule("even", nil),
})
_ = check
_ = errStruct-level custom rules can inspect the current field and same-level fields:
v := validate.New().WithStructRuleCompiler("matchesField", func(rule validate.Rule) (validate.StructRuleFunc, error) {
fieldName, _ := rule.Args["value"].(string)
return func(ctx validate.StructRuleContext) error {
other, _ := ctx.FieldValue(fieldName)
if ctx.Value != other {
return validate.Errors{{Code: "field.matches", Msg: "must match"}}
}
return nil
}, nil
})Global plugin rule:
func init() {
types.RegisterRule("countryCode", compileCountryCode)
translator.RegisterDefaultEnglishTranslations(map[string]string{
"string.country.invalid": "invalid country code",
})
}Custom domain validators should return stable codes, avoid echoing submitted
values in messages, and document whether they are syntax-only or authoritative
business checks. The custom:name=value tag grammar passes a single raw string
argument in rule.Args["value"]; use a custom parser inside the compiler when
you need more structure.
Global rule, type, and translation registration is process-wide and intended
primarily for plugins. Duplicate names overwrite earlier registrations. For
application code and tests, prefer WithRuleCompiler, WithContextRuleCompiler,
WithStructRuleCompiler, WithTypeValidator, and WithTranslator.
Custom types can be registered per validator with WithTypeValidator, then
used with v.CustomType("name"), a matching tag on that validator, or nested
collection tags such as slice;foreach=(name), map;keys=(name), and
map;values=(name). Global plugin-style registration with
types.RegisterGlobalType is still supported, but it is process-wide:
registration order matters, and duplicate names overwrite earlier factories. A
per-instance type registration takes precedence over a global type with the
same name.
CompileRules caches deterministic AST rules. Rules containing function
arguments skip the cache, including nested function arguments. Opaque custom
argument values are serialized with their string form for cache keys, so custom
rule authors should prefer simple stable arguments or use function arguments
when identity or mutable state matters.
The root package includes universal, zero-dependency format validators. Regional, authoritative, or dependency-heavy validators such as postal-code databases, national ID rules, phone-number metadata, currency registries, cron interpreters, and JSON Schema export should live in custom validators or optional plugin packs.
The repository has no Makefile. CI-equivalent checks are:
go mod tidy
go vet ./...
govulncheck ./...
go test ./... -race -covermode=atomic -coverprofile=coverage.out
go tool cover -func=coverage.out
scripts/fuzz.sh