- verax: Validation for Go
verax from Latin truthful, is a flexible and intuitive Go module for
validating data structures, including primitive types, structs, slices, arrays,
and maps. It offers a simple API to define validation rules and produce clear,
human-readable error messages as well as JSON serializable error for easy API
integration. Whether validating user input, configuration data, or complex
nested structures, verax simplifies enforcing constraints and ensuring data
integrity.
- Simple API: Validate data with
verax.Validateorverax.ValidateStructfor structs. - Built-In Rules: Includes multiple built-in rules like
Required,Min,Max,Length, and more for common validation needs. - Informative Errors: Provides human-readable errors with error codes.
- JSON Serializable Errors: Errors serialize to JSON for API integration.
- Struct Tag Support: Customize error message field names using struct tags (e.g.,
jsonor custom tags). - Validator Interface: Implement
verax.Validatorfor custom struct validation logic. - Complex Types: Validate slices, arrays, and maps with aggregated error reporting.
- Extensibility: Create custom rules implementing
verax.Ruleinterface, or by usingverax.Setandverax.By. - Conditional Validation: Use
verax.Whenandverax.Skipfor validation logic.
To use verax in your Go project, install it with:
go get github.com/ctx42/veraxThe verax.Validate function validates primitive types like int, string,
or float64. Pass the value and a list of rules. Rules are evaluated in order,
and the function returns an error for the first rule that fails.
err := verax.Validate(
45,
verax.Required,
verax.Min(42),
verax.Max(44),
)
PrintError(err) // Helper for formatting error output, see (examples_test.go).
PrintJSON(err) // Helper for formatting JSON output, see (examples_test.go).
// Output:
// ERROR:
//
// - must be no greater than 44
//
// JSON:
// {
// "code": "ECInvThreshold",
// "error": "must be no greater than 44"
// }In this example, the value 45 is checked to be non zero-value (Required),
at least 42 (Min), and no more than 44 (Max). It fails the Max(44)
rule. The example also shows the descriptive error message and JSON output.
The verax.ValidateStruct function validates struct fields. Pass a pointer to
the struct and a list of verax.FieldRules, each specifying a field and its
rules. Field names in errors default to the struct field name or json tag,
but can be customized with .Tag().
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 no less than 0
// - name: the length must be between 4 and 7
// - position: must be no greater than 8
//
// JSON:
// {
// "Life": {
// "code": "ECInvThreshold",
// "error": "must be no less than 0"
// },
// "name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// },
// "position": {
// "code": "ECInvThreshold",
// "error": "must be no greater than 8"
// }
// }This validates a Planet struct where:
Positionmust be between 1 and 8,Namemust have between 4 and 7 characters long,Lifemust be between 0.0 and 1.0.
In the above example all fields fail, and errors are presented with names
defined in json tag or struct field name if it was not defined.
By default, error messages use struct field name for the fields in the error
messages unless there is the json tag defined. Use .Tag() to specify a
custom tag for a field.
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"
// }
// }Here, the Name field’s error uses the solar tag name (planet_name)
instead of the json tag name (name).
Structs can implement the verax.Validator interface to define custom
validation logic, reusable across the application:
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)),
)
}Validate the struct:
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 no greater than 8
//
// JSON:
// {
// "planet_name": {
// "code": "ECInvLength",
// "error": "the length must be between 4 and 7"
// },
// "position": {
// "code": "ECInvThreshold",
// "error": "must be no greater than 8"
// }
// }This approach encapsulates validation logic within the struct, ideal for consistent validation across multiple uses.
The verax.Validate supports slices and arrays of structs implementing
verax.Validator. Each element is validated, with errors prefixed by the index
or key.
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 no greater than 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": "ECInvThreshold",
// "error": "must be no greater than 8"
// }
// }Each Planet in the slice is validated using its Validate method. Errors
include the index (e.g., 0.planet_name).
Maps with structs implementing verax.Validator can be validated with
verax.Validate. 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 no greater than 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": "ECInvThreshold",
// "error": "must be no greater than 8"
// }
// }Each Planet is validated, with errors prefixed by the map key (e.g.,
mer.planet_name).
Use verax.Map to individually assign validators to map keys-values.
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)
// or
err = MyRule.Validate(data)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - bool: must be equal to 'true'
// - float: must be no less than 4.2
// - int: must be no greater than 42
// - time: must be no less than 2025-01-01T00:00:00Z
//
// JSON:
// {
// "bool": {
// "code": "ECNotEqual",
// "error": "must be equal to 'true'"
// },
// "float": {
// "code": "ECInvThreshold",
// "error": "must be no less than 4.2"
// },
// "int": {
// "code": "ECInvThreshold",
// "error": "must be no greater than 42"
// },
// "time": {
// "code": "ECInvThreshold",
// "error": "must be no less than 2025-01-01T00:00:00Z"
// }
// }This validates a map with mixed types, with errors prefixed by the key.
verax offers three ways to create custom validation rules:
- Implement the
verax.RuleInterface. - Reuse Existing Rules.
- Custom Validation Functions.
Create a custom rule by implementing verax.Rule:
type UserDoesNotExistRule struct{}
func (u UserDoesNotExistRule) Validate(v any) error {
username, err := verax.EnsureString(v)
if err != nil {
return verax.ErrInvType
}
// Check if the username exists in a database.
err := fmt.Errorf("user %s already exists", username)
return xrr.Wrap(err, xrr.WithCode("ECMustNotExist"))
}Use the rule:
err := verax.Validate("thor", verax.Required, UserDoesNotExistRule{})
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - user thor already existexist
//
// JSON:
// {
// "code": "ECMustNotExist",
// "error": "user thor already existexist"
// }This is ideal for rules requiring external checks, like database queries.
Use verax.Set to group rules for reuse:
NameRule := verax.Set{
verax.Required,
verax.Length(4, 5),
}
err := NameRule.Validate("abc")
// 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"
// }This creates a reusable NameRule for non-empty strings with length between
four and five characters.
Define a function with signature matching func(v any) error and use
verax.By:
fn := func(v any) error {
str, err := verax.EnsureString(v)
if err != nil {
return verax.ErrInvType
}
if str != "" && str != "abc" {
return xrr.New("i need abc", "ECMustABC")
}
return nil
}
AbcRule := verax.By(fn)
err := AbcRule.Validate("xyz")
// or
err = verax.Validate("xyz", AbcRule)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - i need abc
//
// JSON:
// {
// "code": "ECMustABC",
// "error": "i need abc"
// }This is useful for simple, one-off validation logic. Notice that the function
should not return errors for nils or zero-values. This is because verax
has a special rule for that: verax.Required.
Customize error messages and codes with .Error() and .Code():
custom := xrr.New("must be my favorite number", "EC42")
rule := verax.Equal(42).Error(custom)
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:
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'"
// }These methods allow tailoring errors to your application’s needs.
Use verax.When for conditional rules, with an optional Else clause:
r := Range{Start: 44, End: 42}
ErrRange := xrr.New("the end must be greater than the start", "ECRange")
err := verax.ValidateStruct(
&r,
verax.Field(&r.End, verax.When(r.End < r.Start, verax.Error(ErrRange))),
)
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"
// }
// }This checks if Range.End is less than range.Start, applying the error if
true.
Most of the rules in the verax package have When method which accepts a
condition to run it or not.
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 no less than 100
//
// JSON:
// {
// "End": {
// "code": "ECInvThreshold",
// "error": "must be no less than 100"
// }
// }This checks if Range.Start is greater than 50, and if so, applies the Min
rule to Range.End.
Use verax.Skip to skip subsequent rules if a condition is met:
r := Range{Start: 0, End: 0}
ErrRequiredBoth := xrr.New("both values must be set", "ECRange")
err := verax.ValidateStruct(
&r,
verax.Field(
&r.End,
verax.Skip.When(r.Start > 0 && r.End > 0),
verax.Error(ErrRequiredBoth),
),
)
PrintError(err)
PrintJSON(err)
// Output:
// ERROR:
//
// - End: both values must be set
//
// JSON:
// {
// "End": {
// "code": "ECRange",
// "error": "both values must be set"
// }
// }The error triggers only if Range.Start and Range.End are both zero.
verax provides rules for common validation scenarios:
Nil: Ensures a value isnil.Empty: Ensures a value is notnilbut holdszero-value.NotNil: Ensures a value is notnil.Required: Ensures a value is notniland notzero-value.NotEmpty: Ensures a value is notzero-valuewhennon-nil. Allowsnilvalues.By: Creates a rule from afunc(v any) error.Contain: Checks if a value is in a list usingEqual.Each: Applies rules to each element of an array, slice, or map.Equal: Ensures a value equals a specified value.NotEqual: Ensures a value does not equal a specified value.EqualBy: Checks equality with a custom function.Error: Always fails with a specified error.In: Ensures a value is in a specified list.NotIn: Ensures a value is not in a specified list.Length: Ensures a value’s length is within a range.Map: Validates map keys with provided rules.Match: Ensures a value matches a regular expression.Min: Ensures a value is at least a specified value.Max: Ensures a value is at most a specified value.Type: Ensures a value is of a specified type.Noop: A rule that always passes.Skip: Skips subsequent rules if a condition is met.When: Applies rules conditionally, with optionalElse.
See GoDoc for details.
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.