- verax: Validation for Go
- Quick Start
- Features
- Installation
- Usage
- List of Built-In Rules
- Disclaimer
verax (Latin for "truthful") is a Go validation library for primitive types,
structs, slices, arrays, and maps. It produces human-readable,
JSON-serializable errors suitable for direct use in API responses.
import "github.com/ctx42/verax"
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func (r *CreateUserRequest) Validate() error {
return verax.ValidateStruct(r,
verax.Field(&r.Name, verax.Required, verax.Length(2, 50)),
verax.Field(&r.Email, verax.Required, verax.Match(emailRx)),
verax.Field(&r.Age, verax.Required, verax.Min(18), verax.Max(120)),
)
}
req := &CreateUserRequest{Name: "A", Email: "bad", Age: 15}
if err := req.Validate(); err != nil {
data, _ := json.Marshal(err)
// {
// "age": {"code": "ECInvRange", "error": "must be greater or equal to 18"},
// "email": {"code": "ECInvMatch", "error": "must be in a valid format value"},
// "name": {"code": "ECInvLength", "error": "the length must be between 2 and 50"}
// }
}- Simple API:
verax.Validatefor values,verax.ValidateStructfor structs. - Collect All Errors:
ValidateStructreports every failing field in one pass. - Built-In Rules:
Required,Min,Max,Length,Equal,Match,In,Each,Map, and more. - JSON-Ready Errors: All error types implement
json.Marshalerandjson.Unmarshaler— marshal directly into API responses. - Error Classification:
IsError,IsValidationError, andIsInternalErrordistinguish error categories without type assertions. - Struct Tag Support: Field names in errors default to the
jsontag; override with.Tag(). - Validator Interface: Structs implement
verax.Validatorfor self-contained, reusable validation logic. - Complex Types: Validate slices, arrays, and maps with per-element error reporting.
- Extensible: Implement
verax.Rule, compose withverax.Set, or wrap functions withverax.By. - Conditional Validation:
verax.When,verax.Skip, and per-rule.When()for fine-grained control.
go get github.com/ctx42/veraxverax.Validate validates a single value against a list of rules. Rules run in
order and the function returns on the first failure. PrintError and PrintJSON
are helpers defined in examples_test.go.
err := verax.Validate(
45,
verax.Required,
verax.Min(42),
verax.Max(44),
)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - must be less or equal to 44
//
// JSON:
// {
// "code": "ECInvRange",
// "error": "must be less or equal to 44"
// }verax.ValidateStruct validates all fields in one pass, collecting every
failure before returning. Pass a pointer to the struct and verax.Field
descriptors, each specifying a field and its rules. Field names in errors
default to the json tag, or the struct field name when no tag is defined.
Define a struct:
type Planet struct {
Position int `json:"position"`
Name string `json:"name" solar:"planet_name"`
Life float64
}Validate an instance:
planet := Planet{9, "PlanetXYZ", -1}
err := verax.ValidateStruct(
&planet,
verax.Field(&planet.Position, verax.Min(1), verax.Max(8)),
verax.Field(&planet.Name, verax.Length(4, 7)),
verax.Field(&planet.Life, verax.Min(0.0), verax.Max(1.0)),
)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - Life: must be greater or equal to 0
// - name: the length must be between 4 and 7
// - position: must be less or equal to 8
//
// JSON:
// {
// "Life": {
// "code": "ECInvRange",
// "error": "must be greater or equal to 0"
// },
// "name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// },
// "position": {
// "code": "ECInvRange",
// "error": "must be less or equal to 8"
// }
// }Use .Tag() to pick a different struct tag for the field name in errors:
planet := Planet{1, "Mer", 0.0}
err := verax.ValidateStruct(
&planet,
verax.Field(&planet.Name, verax.Length(4, 7)).Tag("solar"),
)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - planet_name: the length must be between 4 and 7
//
// JSON:
// {
// "planet_name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// }
// }Structs that implement verax.Validator are validated automatically when
passed to verax.Validate, or when they appear as elements in a slice, array,
or map:
func (p *Planet) Validate() error {
return verax.ValidateStruct(
p,
verax.Field(&p.Position, verax.Min(1), verax.Max(8)),
verax.Field(&p.Name, verax.Length(4, 7)).Tag("solar"),
verax.Field(&p.Life, verax.Min(0.0), verax.Max(1.0)),
)
}planet := &Planet{9, "Mer", 0.0}
err := planet.Validate()
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - planet_name: the length must be between 4 and 7
// - position: must be less or equal to 8
//
// JSON:
// {
// "planet_name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// },
// "position": {
// "code": "ECInvRange",
// "error": "must be less or equal to 8"
// }
// }Slices and arrays of verax.Validator values are validated element by element.
Errors are prefixed with the element index.
planets := []*Planet{
{1, "Mer", 0},
{3, "Earth", 1.0},
{9, "X", 0.1},
}
err := verax.Validate(planets)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - 0.planet_name: the length must be between 4 and 7
// - 2.planet_name: the length must be between 4 and 7
// - 2.position: must be less or equal to 8
//
// JSON:
// {
// "0.planet_name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// },
// "2.planet_name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// },
// "2.position": {
// "code": "ECInvRange",
// "error": "must be less or equal to 8"
// }
// }Maps whose values implement verax.Validator are validated the same way.
Errors are prefixed with the map key.
planets := map[string]*Planet{
"mer": {1, "Mer", 0},
"ear": {3, "Earth", 1.0},
"x": {9, "X", 0.1},
}
err := verax.Validate(planets)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - mer.planet_name: the length must be between 4 and 7
// - x.planet_name: the length must be between 4 and 7
// - x.position: must be less or equal to 8
//
// JSON:
// {
// "mer.planet_name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// },
// "x.planet_name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// },
// "x.position": {
// "code": "ECInvRange",
// "error": "must be less or equal to 8"
// }
// }Use verax.Map to assign rules to specific map keys:
data := map[string]any{
"bool": false,
"int": 44,
"float": 0.1,
"time": time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
}
MyRule := verax.Map(
verax.Key("bool", verax.Equal(true)),
verax.Key("int", verax.Max(42)),
verax.Key("float", verax.Min(4.2)),
verax.Key("time", verax.Min(
time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
)),
)
err := verax.Validate(data, MyRule) // nolint: ineffassign
// or
err = MyRule.Validate(data)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - bool: must be equal to 'true'
// - float: must be greater or equal to 4.2
// - int: must be less or equal to 42
// - time: must be greater or equal to 2025-01-01T00:00:00Z
//
// JSON:
// {
// "bool": {
// "code": "ECNotEqual",
// "error": "must be equal to 'true'"
// },
// "float": {
// "code": "ECInvRange",
// "error": "must be greater or equal to 4.2"
// },
// "int": {
// "code": "ECInvRange",
// "error": "must be less or equal to 42"
// },
// "time": {
// "code": "ECInvRange",
// "error": "must be greater or equal to 2025-01-01T00:00:00Z"
// }
// }verax offers three ways to create custom validation rules:
- Implement the
verax.Ruleinterface. - Compose existing rules with
verax.Set. - Wrap a plain function with
verax.By.
Implement verax.Rule when you need full control, such as querying a database.
The Validate method must return:
nilon success,*verax.Errorfor a user-facing validation failure, or*verax.InternalErrorfor misuse (e.g. a caller passed the wrong type).
type UserDoesNotExistRule struct{}
func (u UserDoesNotExistRule) Validate(have any) error {
username, ok := have.(string)
if !ok {
return verax.NewInternalError("must be a string", verax.ECInvType)
}
if userExists(username) {
return verax.NewError(
fmt.Sprintf("user %s already exist", username),
"ECMustNotExist",
)
}
return nil
}Use the rule:
err := verax.Validate("thor", verax.Required, UserDoesNotExistRule{})
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - user thor already exist
//
// JSON:
// {
// "code": "ECMustNotExist",
// "error": "user thor already exist"
// }Use verax.Set to compose rules for reuse:
NameRule := verax.Set{
verax.Required,
verax.Length(4, 5),
}
err := NameRule.Validate("abc") // nolint: ineffassign
// or
err = verax.Validate("abc", NameRule)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - the length must be between 4 and 5
//
// JSON:
// {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 5"
// }Wrap a func(v any) error with verax.By:
fn := func(v any) error {
str, err := verax.EnsureString(v)
if err != nil {
return verax.ErrInvType
}
if str != "" && str != "abc" {
return verax.NewError("i need abc", "ECMustABC")
}
return nil
}
AbcRule := verax.By(fn)
err := AbcRule.Validate("xyz") // nolint: ineffassign
// or
err = verax.Validate("xyz", AbcRule)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - i need abc
//
// JSON:
// {
// "code": "ECMustABC",
// "error": "i need abc"
// }Tip: Custom functions should return
nilfor nil or zero values. Useverax.Requiredas a separate rule to enforce non-zero values.
Customize error messages and codes with .Message() and .Code():
rule := verax.Equal(42).Message("must be my favorite number").Code("EC42")
err := verax.Validate(44, rule)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - must be my favorite number
//
// JSON:
// {
// "code": "EC42",
// "error": "must be my favorite number"
// }Customize only the error code, keeping the default message:
rule := verax.Equal(42).Code("EC42")
err := verax.Validate(44, rule)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - must be equal to '42'
//
// JSON:
// {
// "code": "EC42",
// "error": "must be equal to '42'"
// }verax uses three distinct error types, all in the same error domain:
| Type | Meaning |
|---|---|
*verax.Error |
Single-value validation failure (user-facing) |
*verax.FieldsError |
Multi-field validation failure from ValidateStruct (user-facing) |
*verax.InternalError |
Library misuse — wrong type, incomplete rule setup, etc. |
Three predicate functions classify errors without type assertions:
err := someValidation()
// IsValidationError is true for *Error and *FieldsError — safe to return to
// the client.
if verax.IsValidationError(err) {
// handle user-facing validation failure
}
// IsInternalError is true for *InternalError — indicates a programming
// mistake; log it and return a 500.
if verax.IsInternalError(err) {
log.Println("bug: internal validation error:", err)
}
// IsError is true for any verax error (the union of the two above).
if verax.IsError(err) {
// handle any verax error
}The three functions satisfy the invariant:
IsError(err) == IsValidationError(err) || IsInternalError(err)
All three error types implement json.Marshaler and json.Unmarshaler. Pass
a verax error directly to json.Marshal or any JSON encoder — no manual
conversion is needed.
A single validation error (*verax.Error):
{"error": "must be no greater than 44", "code": "ECInvRange"}With optional metadata attached via xrr.Meta:
{"error": "must be no greater than 44", "code": "ECInvRange", "meta": {"field": "age"}}A struct validation error (*verax.FieldsError) from ValidateStruct:
{
"name": {"error": "the length must be between 4 and 7", "code": "ECInvLength"},
"position": {"error": "must be less or equal to 8", "code": "ECInvRange"}
}A typical HTTP handler using verax errors directly:
func CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if err := req.Validate(); err != nil {
if verax.IsInternalError(err) {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(err) // marshals to structured JSON automatically
return
}
// process the valid request ...
}Because errors also implement json.Unmarshaler, they can be decoded back from
JSON — useful for forwarding validation errors between services or for
client-side processing.
Use errors.AsType (Go 1.25+) to obtain a typed handle on a validation error:
err := verax.ValidateStruct(&planet, ...)
// Iterate over field-level failures.
if fe, ok := errors.AsType[*verax.FieldsError](err); ok {
for field, fieldErr := range fe.ErrorFields() {
fmt.Println(field, fieldErr)
}
}
// Read the error code from a single validation error.
if e, ok := errors.AsType[*verax.Error](err); ok {
fmt.Println(e.ErrorCode())
}
// Handle an internal error (library misuse).
if _, ok := errors.AsType[*verax.InternalError](err); ok {
log.Println("internal validation error:", err)
}Use verax.When for conditional rules, with an optional Else clause:
r := Range{Start: 44, End: 42}
failRule := verax.Fail("the end must be greater than the start", "ECRange")
err := verax.ValidateStruct(
&r,
verax.Field(&r.End, verax.When(r.End < r.Start, failRule)),
)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - End: the end must be greater than the start
//
// JSON:
// {
// "End": {
// "code": "ECRange",
// "error": "the end must be greater than the start"
// }
// }Most built-in rules also have a .When(condition bool) method for inline
conditioning:
r := Range{Start: 51, End: 42}
err := verax.ValidateStruct(
&r,
verax.Field(&r.End, verax.Min(100).When(r.Start > 50)),
)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - End: must be greater or equal to 100
//
// JSON:
// {
// "End": {
// "code": "ECInvRange",
// "error": "must be greater or equal to 100"
// }
// }Use verax.Skip to skip subsequent rules when a condition is met:
r := Range{Start: 0, End: 0}
err := verax.ValidateStruct(
&r,
verax.Field(
&r.End,
verax.Skip.When(r.Start > 0 && r.End > 0),
verax.Fail("both values must be set", "ECRange"),
),
)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - End: both values must be set
//
// JSON:
// {
// "End": {
// "code": "ECRange",
// "error": "both values must be set"
// }
// }| Rule | Description |
|---|---|
Required |
Value must not be nil and not a zero value. |
Nil |
Value must be nil. |
NotNil |
Value must not be nil. |
Empty |
Value must be a non-nil zero value. |
NotEmpty |
Value must be non-zero when non-nil; nil is allowed. |
Equal(want) |
Value must equal want. |
NotEqual(want) |
Value must not equal want. |
Min(n) |
Value must be ≥ n. |
Max(n) |
Value must be ≤ n. |
Length(min, max) |
Length must be in [min, max]. |
Match(rx) |
Value must match the regular expression rx. |
In(values...) |
Value must be one of values. |
NotIn(values...) |
Value must not be in values. |
Contain(values...) |
Value must contain all of values. |
Each(rules...) |
Apply rules to every element of a slice, array, or map. |
Map(keys...) |
Apply per-key rules to a map (see verax.Key). |
Type(t) |
Value must be of reflect type t. |
By(fn) |
Wrap a func(have any) error as a rule. |
Check(fn, msg, code) |
Wrap a typed func[T any](v T) bool as a rule with a fixed message and code. |
Set{rules...} |
Compose rules into a reusable group. |
Fail(msg, code) |
Always fails with the given message and code. |
Noop |
Always passes (useful as a placeholder). |
When(cond, rules...) |
Apply rules only when cond is true. |
Skip |
Skip subsequent rules; combine with .When() for conditions. |
Additional rules for network addresses (IP, IPv4, IPv6, port, DNS, domain,
host), Base64, and Semantic Versioning live in pkg/verax/rule.
See GoDoc for the complete API reference.
The verax API was inspired by
github.com/go-ozzo/ozzo-validation
but was built from scratch with a different API and features, using
github.com/ctx42/xrr for error handling.