Skip to content

SladeThe/yav

Repository files navigation

Yet Another Validator

Go struct and field validation.

The project is inspired by go-playground/validator and uses some of its codebase.
YAV aims to provide Playground-like validator configuration and produce compatible errors whenever possible.
At the same time, the introduced chained validator improves validation speed dramatically.
The Playground validator's performance is quite poor due to heavy reflection usage, which YAV strives not to use.

YAV's key principles:

  • mimic Playground validator whenever possible;
  • make fewer to zero allocations;
  • work fast.

The main drawback of other builder-like validators (e.g. ozzo) is that they are interface{}-based and allocate a lot of memory upon each run. Sometimes, it even makes them slower than the Playground validator.

Unlike in earlier versions, the repo no longer includes Playground validator wrapper in order to reduce the number of 3rd-party dependencies. The removed code still can be found in yav-tests.

Examples

Single field struct validation

The field name passed to yav.Chain doesn't affect the validation process and is only necessary for building a validation error, so that you may use whatever naming style you like, i.e. id, ID, Id.

type AccountID struct {
	ID string
}

func (id AccountID) Validate() error {
	return yav.Chain(
		"id", id.ID,
		vstring.Required,
		vstring.UUID,
	)
}

Combine validation errors

Use yav.Join to combine multiple validation errors.

type Password struct {
	Salt, Hash []byte
}

func (p Password) Validate() error {
	return yav.Join(
		yav.Chain(
			"salt", p.Salt,
			vbytes.Required,
			vbytes.Max(200),
		),
		yav.Chain(
			"hash", p.Hash,
			vbytes.Required,
			vbytes.Max(200),
		),
	)
}

Validate nested structs

Use yav.Nested to add value namespace, i.e. to get password.salt error instead of just salt.
Contrary, any possible id error is returned as if the field were in the Account struct directly.

type Account struct {
	AccountID

	Login    string
	Password Password
}

func (a Account) Validate() error {
	return yav.Join(
		a.AccountID.Validate(),
		yav.Chain(
			"login", a.Login,
			vstring.Required,
			vstring.Between(4, 20),
			vstring.Alphanumeric,
			vstring.Lowercase,
		),
		yav.Nested("password", a.Password.Validate()),
	)
}

Compare YAV and Playground validator

Here we pass to YAV Go-like field names in order to produce Playground-compatible errors.
YAV doesn't anyhow use it, except while building validation errors.
If compatibility is not required, pass the field names in whatever style you prefer.

type Account struct {
    ID string `validate:"required,uuid"`
    
    Login    string `validate:"required,min=4,max=20,alphanum,lowercase"`
    Password string `validate:"required_with=Login,omitempty,min=8,max=32,text"`
    
    Email string `validate:"required,min=6,max=100,email"`
    Phone string `validate:"required,min=8,max=16,e164"`
}

func (a Account) Validate() error {
	return yav.Join(
		yav.Chain(
			"ID", a.ID,
			vstring.Required,
			vstring.UUID,
		),
		yav.Chain(
			"Login", a.Login,
			vstring.Required,
			vstring.Min(4),
			vstring.Max(20),
			vstring.Alphanumeric,
			vstring.Lowercase,
		),
		yav.Chain(
			"Password", a.Password,
			vstring.RequiredWithAny().String(a.Login).Names("Login"),
			vstring.Between(8, 32),
			vstring.Text,
		),
		yav.Chain(
			"Email", a.Email,
			vstring.Required,
			vstring.Between(6, 100),
			vstring.Email,
		),
		yav.Chain(
			"Phone", a.Phone,
			vstring.Required,
			vstring.Between(8, 16),
			vstring.E164,
		),
	)
}

Available validations

Common

OmitEmpty
Required
RequiredIf
RequiredUnless
RequiredWithAny
RequiredWithoutAny
RequiredWithAll
RequiredWithoutAll
ExcludedIf
ExcludedUnless
ExcludedWithAny
ExcludedWithoutAny
ExcludedWithAll
ExcludedWithoutAll

Bool

Equal
NotEqual

Bytes

Min
Max
Between

Duration

Min
Max
Between
LessThan
LessThanOrEqual
GreaterThan
GreaterThanOrEqual

LessThanNamed
LessThanOrEqualNamed
GreaterThanNamed
GreaterThanOrEqualNamed

Map

Min
Max
Between

Unique

Keys
Values

Number

Min
Max
Between
LessThan
LessThanOrEqual
GreaterThan
GreaterThanOrEqual

Equal
NotEqual
OneOf

Slice

Min
Max
Between

Unique

Items

String

Min
Max
Between

Equal
NotEqual
OneOf

Alpha
Alphanumeric
Lowercase
Uppercase
ContainsAlpha
ContainsLowerAlpha
ContainsUpperAlpha
ContainsDigit
ContainsSpecialCharacter
ExcludesWhitespace
StartsWithAlpha
StartsWithLowerAlpha
StartsWithUpperAlpha
StartsWithDigit
StartsWithSpecialCharacter
EndsWithAlpha
EndsWithLowerAlpha
EndsWithUpperAlpha
EndsWithDigit
EndsWithSpecialCharacter

Text
Title

E164
Email
Hostname
HostnameRFC1123
HostnamePort
FQDN
URI
URL
UUID

Regexp

Time

Min
Max
Between
LessThan
LessThanOrEqual
GreaterThan
GreaterThanOrEqual

LessThanNamed
LessThanOrEqualNamed
GreaterThanNamed
GreaterThanOrEqualNamed

Benchmarks

goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i9-10850K CPU @ 3.60GHz

Tiny struct validation

BenchmarkYAV              12907930       92.15 ns/op          0 B/op        0 allocs/op
BenchmarkOzzo              1334562       890.1 ns/op       1248 B/op       20 allocs/op
BenchmarkPlayground        1324868       911.8 ns/op         40 B/op        2 allocs/op

Account struct validation

BenchmarkYAV                729123        1658 ns/op        123 B/op        4 allocs/op
BenchmarkPreAllocatedYAV    802777        1488 ns/op          0 B/op        0 allocs/op
BenchmarkOzzo*               54954       21684 ns/op      19215 B/op      317 allocs/op
BenchmarkPlayground         172633        6789 ns/op        653 B/op       23 allocs/op

Notes

  • The Account in the Examples section is a reduced version of the benchmarked structure.
  • Ozzo validator lacks some features available in both YAV and Playground validator. Therefore, those validation steps were not enabled for ozzo.
  • The YAV is still slower, than a manually written validation boilerplate, but the amount of code differs dramatically.