Validator is an amazing project. But in applications with high frequency use, the way that validator works (i.e. reflection) can cause performance penalty.
ValidGen born to solve that gap. Instead of use reflection, ValidGen uses the code generating approach.
At this time it is an unstable project and should not be used in production environments.
ValidGen internals follows the current generator from the parser through the analyzer, code generator, and package writer, and says where TestGen fits.
The following requirements are needed to build the project:
- Git
- Go >= 1.24
- Make
The steps to build are:
# Clone the project repository
git clone git@github.com:opencodeco/validgen.git
# Enter in the project root folder
cd validgen
# Build the binary
make buildAfter that the executable will be in bin/validgen.
Push a tag named vMAJOR.MINOR.PATCH, with an optional pre-release suffix such as -rc.1. That tag push runs Release on a GitHub-hosted runner. The workflow cross-compiles ValidGen with CGO_ENABLED=0 and attaches the binaries to the GitHub release for that tag. Pull requests do not publish releases.
The release assets are:
validgen_v1.2.3_linux_amd64validgen_v1.2.3_linux_arm64validgen_v1.2.3_darwin_amd64validgen_v1.2.3_darwin_arm64SHA256SUMS
amd64 is the Intel 64-bit build. arm64 is the Arm 64-bit build.
By default ValidGen only generates TValidate functions. Pass -unmarshal-json to also generate encoding/json.Unmarshaler methods that decode with an alias (to avoid recursion) and then validate:
./bin/validgen -unmarshal-json ./mypackageGenerated methods look like:
func (obj *User) UnmarshalJSON(b []byte) error {
type alias User
if err := json.Unmarshal(b, (*alias)(obj)); err != nil {
return err
}
if errs := UserValidate(obj); len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}Malformed JSON still returns the usual encoding/json decode error. Validation failures are returned via errors.Join over the []error from UserValidate.
Struct tags may use valid or validate. When both keys are present, valid is the one that is read. The common go-playground example uses validate.
The following validations will be implemented:
- eq (equal): must be equal to the specified value
- eq_ignore_case (equal ignoring case): must be equal to the specified value (ignoring case)
- gt (greater than): must be > to the specified value
- gte (greater than or equal): must be >= to the specified value
- lt (less than): must be < to the specified value
- lte (less than or equal): must be <= to the specified value
- neq (not equal): must not be equal to the specified value
- neq_ignore_case (not equal ignoring case): must not be equal to the specified value (ignoring case)
- len (length): must have the following length
- max (max): must have no more than max characters
- min (min): must have no less than min characters
- in (in): must be one of the following values
- nin (not in): must not be one of the following values
- required (required): is required
- email (email): must be a valid email format (empty is valid for optional fields)
- oneof (one of): string or integer must be one of the space-separated values
- hexcolor, rgb, rgba, hsl, hsla: string must match that color format
- iscolor: alias for hexcolor, rgb, rgba, hsl, or hsla
- eqfield (equal field): field must be equal to another field
- neqfield (not equal field): field must not be equal to another field
- gtefield (greater than or equal field): field must be greater than or equal to another field
- gtfield (greater than field): field must be greater than another field
- ltefield (less than or equal field): field must be less than or equal to another field
- ltfield (less than field): field must be less than another field
- dive: tags after
diveapply to each slice element, array element, or map value - keys, endkeys: after
diveon a map, tags betweenkeysandendkeysapply to each key and tags afterendkeysapply to each value
Tags before dive apply to the collection. Tags after dive apply to each element. A struct element is validated with that struct's generated function. Without dive, a slice or map is checked only as a collection, including a slice of structs.
type User struct {
Addresses []*Address `valid:"required,dive,required"`
Labels map[string]string `valid:"dive,required"`
}required before dive checks that Addresses is not empty. After dive, each pointer must be non-nil and Address field tags run. Each Labels value must be non-empty. Plain dive validates map values.
keys follows dive immediately and applies only to maps. Tags between keys and endkeys validate each map key. Tags after endkeys validate each map value.
type User struct {
Labels map[string]string `valid:"dive,keys,min=2,endkeys,required"`
Scores map[uint8]string `valid:"dive,keys,gte=1,endkeys,required"`
}min=2 checks each Labels key. required checks each Labels value. gte=1 checks each Scores key. A missing endkeys, an extra endkeys, keys on a non-map, dive inside keys, another keys block, and a non-scalar map key are rejected.
The following table shows the validations and possible types, where:
- "I" means "Implemented"
- "W" means "Will be implemented"
- "P" means "Partially implemented"
- "-" means "Will not be implemented"
| Validation/Type | String | Numeric types (integers and floats) | Complex | Boolean | Slice | Array | Map | Time | Duration |
|---|---|---|---|---|---|---|---|---|---|
| eq | I | I | I | I | - | - | - | W | W |
| eq_ignore_case | I | - | - | - | - | - | - | - | - |
| gt | - | I | - | - | - | - | - | W | W |
| gte | - | I | - | - | - | - | - | W | W |
| lt | - | I | - | - | - | - | - | W | W |
| lte | - | I | - | - | - | - | - | W | W |
| neq | I | I | I | I | - | - | - | W | W |
| neq_ignore_case | I | - | - | - | - | - | - | - | - |
| len | I | - | - | - | I | - | W | - | - |
| max | I | - | - | - | I | - | W | W | W |
| min | I | - | - | - | I | - | W | W | W |
| in | I | I | I | - | I | I | W | - | W |
| nin | I | I | I | - | I | I | W | - | W |
| required | I | I | I | - | I | - | W | W | W |
| I | - | - | - | - | - | - | - | - | |
| oneof | I | I | - | - | - | - | - | - | - |
| hexcolor | I | - | - | - | - | - | - | - | - |
| rgb | I | - | - | - | - | - | - | - | - |
| rgba | I | - | - | - | - | - | - | - | - |
| hsl | I | - | - | - | - | - | - | - | - |
| hsla | I | - | - | - | - | - | - | - | - |
| iscolor | I | - | - | - | - | - | - | - | - |
| eqfield | I | I | I | I | - | - | - | W | W |
| neqfield | I | I | I | I | - | - | - | W | W |
| gtefield | - | I | - | - | - | - | - | W | W |
| gtfield | - | I | - | - | - | - | - | W | W |
| ltefield | - | I | - | - | - | - | - | W | W |
| ltfield | - | I | - | - | - | - | - | W | W |
Complex (complex64, complex128) supports eq, neq, in, nin, eqfield, and neqfield via Go == / !=, and required via != 0 (the zero value 0+0i). Tag values are Go imaginary literals without spaces, for example eq=1+2i and in=1+2i 5+6i. Ordering tags (gt, gte, lt, lte, gtfield, gtefield, ltfield, ltefield) are rejected: Go has no < / > / <= / >= for complex values.
The steps to run the unit tests are:
# Enter in the project root folder
cd validgen
# Run the unit tests
make unittestsComparisons with go-playground/validator live in validgen-benchmarks. That module holds the small three-way benchmark, the generated comparison suite, and the color helper check.
git clone git@github.com:opencodeco/validgen-benchmarks.git
cd validgen-benchmarks
make bench
make colormake cmp runs the generated suite. The default bench time is 5 seconds per benchmark. make cmp BENCH_TIME=100ms is a shorter pass.
The steps to run the end-to-end tests are:
# Enter in the project root folder
cd validgen
# Run the end-to-end tests
make endtoendtestsExamples live in opencodeco/validgen-samples.
That repository is a separate Go module. It shows the ValidGen CLI, the generated import of github.com/opencodeco/validgen/types, and a caller importing the package that owns the structs.
Build the CLI from this repository with make build. The binary is bin/validgen. It takes one path and writes validator__.go next to the structs.
./bin/validgen /path/to/packagePass -unmarshal-json to also generate UnmarshalJSON methods. The samples repository shows that flag on signup.
These numbers are from an Apple M4 Pro, 12 cores, running go test -bench=. -benchmem -benchtime=5s on the generated suite:
goos: darwin
goarch: arm64
pkg: github.com/opencodeco/validgen-benchmarks/cmp
cpu: Apple M4 Pro (12 Cores used)The following table as the performance results:
| Test name | ValidGen | GoValidator | Performance |
|---|---|---|---|
| StringRequired | 5.000 ns/op | 40.12 ns/op | 8.02x |
| StringEq | 5.000 ns/op | 39.90 ns/op | 7.98x |
| StringEqIC | 14.96 ns/op | 40.63 ns/op | 2.71x |
| StringNeq | 5.000 ns/op | 40.65 ns/op | 8.13x |
| StringNeqIC | 5.000 ns/op | 41.05 ns/op | 8.21x |
| StringLen | 5.000 ns/op | 44.35 ns/op | 8.87x |
| StringMax | 5.000 ns/op | 45.15 ns/op | 9.03x |
| StringMin | 5.000 ns/op | 45.10 ns/op | 9.02x |
| StringIn | 5.000 ns/op | 49.75 ns/op | 9.95x |
| StringEmail | 167.4 ns/op | 436.3 ns/op | 2.60x |
The following table as the raw results:
| Test name | Iterations | Nanoseconds per operation | Number os bytes allocated per operation | Number of allocations per operation |
|---|---|---|---|---|
| BenchmarkValidGenStringRequired-12 | 1.000.000.000 | 5.000 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringRequired-12 | 149.656.641 | 40.12 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringEq-12 | 1.000.000.000 | 5.000 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringEq-12 | 150.419.842 | 39.90 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringEqIC-12 | 401.652.991 | 14.96 ns/op | 8 B/op | 1 allocs/op |
| BenchmarkValidatorStringEqIC-12 | 147.517.011 | 40.63 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringNeq-12 | 1.000.000.000 | 5.000 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringNeq-12 | 147.375.966 | 40.65 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringNeqIC-12 | 1.000.000.000 | 5.000 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringNeqIC-12 | 146.089.116 | 41.05 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringLen-12 | 1.000.000.000 | 5.000 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringLen-12 | 135.221.859 | 44.35 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringMax-12 | 1.000.000.000 | 5.000 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringMax-12 | 133.947.687 | 45.15 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringMin-12 | 1.000.000.000 | 5.000 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringMin-12 | 133.335.433 | 45.10 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringIn-12 | 1.000.000.000 | 5.000 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringIn-12 | 120.405.889 | 49.75 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidGenStringEmail-12 | 35.513.684 | 167.4 ns/op | 0 B/op | 0 allocs/op |
| BenchmarkValidatorStringEmail-12 | 13.740.566 | 436.3 ns/op | 88 B/op | 5 allocs/op |
The steps to run all linters are:
# Enter in the project root folder
cd validgen
# Download and install golang-ci-lint (run just once)
make setup
# Run all enabled linters
make lintYou can find instructions on how to contribute code and bug reports in the CONTRIBUTING guide.
ValidGen uses MIT License.