EVO provides a comprehensive validation system via lib/validation. Validators are defined using struct tags and support both standalone (no DB required) and database-aware rules.
import "github.com/getevo/evo/v2/lib/validation"
Add a validation tag to struct fields, then call validation.Struct:
type CreateUserInput struct {
Name string `json:"name" validation:"required,name,len>=2,len<=100"`
Email string `json:"email" validation:"required,email"`
Password string `json:"password" validation:"required,password(medium)"`
Age int `json:"age" validation:">=18,<=120"`
}
input := CreateUserInput {
Name : "Alice" ,
Email : "alice@example.com" ,
Password : "MyPass1!" ,
Age : 25 ,
}
if errs := validation .Struct (input ); len (errs ) > 0 {
for _ , e := range errs {
fmt .Println (e )
}
}
Validate only non-zero fields (partial update)
// Only validates fields that have a non-zero value
errs := validation .StructNonZeroFields (input )
err := validation .Value ("alice@example.com" , "required,email" )
if err != nil {
fmt .Println (err )
}
Context-aware validation (with timeout)
ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
defer cancel ()
errs := validation .StructWithContext (ctx , input )
errs2 := validation .StructNonZeroFieldsWithContext (ctx , input )
evo .Post ("/api/users" , func (r * evo.Request ) any {
var input CreateUserInput
r .BodyParser (& input )
if errs := validation .Struct (input ); len (errs ) > 0 {
return outcome .UnprocessableEntity (errs )
}
return outcome .Created (createUser (input ))
})
Multiple validators are combined with commas: validation:"required,email,len<=255"
Validator
Description
required
Field must be non-empty
text
No HTML tags allowed
alpha
Letters and spaces only
alphanumeric
Letters, digits, and spaces only
latin
Unicode letters only
digit
Digits only (0-9)
slug
Lowercase letters, digits, hyphens, underscores (1–200 chars)
name
Valid personal name (letters, spaces, . ' -)
ascii
ASCII characters only
printable
Printable characters only
upperCase
Must be all uppercase
lowerCase
Must be all lowercase
no_html
Must not contain HTML tags
safe_html
Must not contain XSS-dangerous HTML
type Post struct {
Slug string `validation:"required,slug"`
Title string `validation:"required,text,len>=3,len<=200"`
Content string `validation:"safe_html"`
}
Validator
Description
len>N
Length strictly greater than N
len>=N
Length greater than or equal to N
len<N
Length strictly less than N
len<=N
Length less than or equal to N
len==N
Exact length N
len!=N
Length not equal to N
Length is measured in Unicode runes (not bytes).
type Input struct {
Username string `validation:"len>=3,len<=20"`
Bio string `validation:"len<=500"`
Code string `validation:"len==6"`
}
Validator
Description
>N
Greater than N
>=N
Greater than or equal to N
<N
Less than N
<=N
Less than or equal to N
==N
Equal to N
!=N
Not equal to N
int
Valid integer
+int
Positive integer
-int
Negative integer
float
Valid float
+float
Positive float
-float
Negative float
type Product struct {
Price float64 `validation:">0"`
Quantity int `validation:">=0,<=10000"`
Rating int `validation:">=1,<=5"`
}
Internet / network validators
Validator
Description
email
Valid email address
url
Valid URL
domain
Valid domain name
ip or ipv4
Valid IPv4 address
ipv6
Valid IPv6 address
cidr
Valid CIDR notation (192.168.1.0/24)
mac
Valid MAC address (AA:BB:CC:DD:EE:FF)
port
Valid port number (1–65535)
phone
Valid phone number
e164
Valid E.164 phone number (+15551234567)
type Server struct {
IP string `validation:"required,ipv4"`
Port int `validation:"required,port"`
Email string `validation:"required,email"`
Website string `validation:"url"`
}
Validator
Description
date
Valid RFC3339 date/time
time
Valid RFC3339 timestamp
unix_timestamp
Valid Unix timestamp integer
duration
Valid Go duration string (1h30m, 500ms)
timezone
Valid IANA timezone (America/New_York)
before_now
Date must be in the past
after_now
Date must be in the future
date_format(layout)
Date matches the Go layout string
type Event struct {
StartAt string `validation:"required,date,after_now"`
EndAt string `validation:"required,date"`
Duration string `validation:"duration"`
Timezone string `validation:"timezone"`
BirthDate string `validation:"before_now"`
ReportDate string `validation:"date_format(2006-01-02)"`
}
Validator
Requirement
password(none)
No requirements
password(easy)
Minimum 6 characters
password(medium)
Min 8 chars, at least 3 of: uppercase, lowercase, digits, symbols
password(hard)
Min 12 chars, all 4: uppercase, lowercase, digits, symbols
type Registration struct {
Password string `validation:"required,password(medium)"`
}
type Input struct {
Code string `validation:"regex([A-Z]{2}[0-9]{4})"`
PostCode string `validation:"regex(^[0-9]{5}(-[0-9]{4})?$)"`
}
Inclusion / exclusion validators
Validator
Description
in(a,b,c)
Value must be one of the listed values
not_in(a,b,c)
Value must not be any of the listed values
contains(str)
Value must contain the substring
not_contains(str)
Value must not contain the substring
starts_with(str)
Value must start with the prefix
ends_with(str)
Value must end with the suffix
type Order struct {
Status string `validation:"in(pending,processing,shipped,delivered)"`
Priority string `validation:"not_in(low,none)"`
Reference string `validation:"starts_with(ORD-)"`
Extension string `validation:"ends_with(.pdf)"`
Notes string `validation:"not_contains(spam)"`
}
Validator
Description
min_items(N)
Slice must have at least N elements
max_items(N)
Slice must have at most N elements
unique_items
All slice elements must be unique
type Form struct {
Tags []string `validation:"min_items(1),max_items(10),unique_items"`
Recipients []string `validation:"min_items(1),max_items(50)"`
}
Validator
Description
json
Valid JSON string
uuid
Valid UUID
hex
Valid hexadecimal string
hex_color
Valid hex color (#RRGGBB)
rgb_color
Valid RGB color (rgb(r,g,b))
rgba_color
Valid RGBA color (rgba(r,g,b,a))
isbn
Valid ISBN (10 digit)
isbn10
Valid ISBN-10
isbn13
Valid ISBN-13
credit_card
Valid credit card number (Luhn check)
iban
Valid IBAN format
btc_address
Valid Bitcoin address
eth_address
Valid Ethereum address
country_alpha2
Valid ISO 3166-1 alpha-2 country code
country_alpha3
Valid ISO 3166-1 alpha-3 country code
cron
Valid cron expression
latitude
Valid latitude (-90 to 90)
longitude
Valid longitude (-180 to 180)
type Payment struct {
CardNumber string `validation:"required,credit_card"`
IBAN string `validation:"iban"`
Currency string `validation:"required,in(USD,EUR,GBP)"`
}
type Location struct {
Lat float64 `validation:"latitude"`
Lng float64 `validation:"longitude"`
}
These validators require a database connection. They are skipped silently when no database is configured.
Validator
Description
unique
Value must be unique in the table column
unique:col1|col2
Combination of columns must be unique
fk
Value must exist in the referenced table (via gorm:"fk:table" tag)
enum
Value must match enum values in the gorm tag
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex" validation:"required,email,unique"`
Role string `gorm:"type:enum('admin','user','guest')" validation:"required,enum"`
}
type Order struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"fk:users" validation:"required,fk"`
ProductID uint `gorm:"fk:products" validation:"required,fk"`
}
Unique with composite columns
type TeamMember struct {
ID uint `gorm:"primaryKey"`
TeamID uint
UserID uint `validation:"unique:team_id|user_id"` // unique together
}
Cross-field validators (DB)
These compare fields within the same struct and require a database connection.
Validator
Description
confirmed
Must match {FieldName}Confirmation field
same(Field)
Must equal the named field
different(Field)
Must differ from the named field
before(Field)
Time must be before the named field
after(Field)
Time must be after the named field
gt_field(Field)
Numeric: must be greater than the named field
gte_field(Field)
Numeric: must be ≥ the named field
lt_field(Field)
Numeric: must be less than the named field
lte_field(Field)
Numeric: must be ≤ the named field
type ChangePassword struct {
Password string `validation:"required,password(medium)"`
PasswordConfirmation string // auto-checked by "confirmed"
OldPassword string `validation:"different(Password)"`
}
type DateRange struct {
StartDate string `validation:"required,date"`
EndDate string `validation:"required,date,after(StartDate)"`
}
type PriceRange struct {
MinPrice float64 `validation:"required,>0"`
MaxPrice float64 `validation:"required,gt_field(MinPrice)"`
}
Register a custom validator
import (
"fmt"
"strings"
"github.com/getevo/evo/v2/lib/validation"
"github.com/getevo/evo/v2/lib/generic"
)
func init () {
validation .RegisterValidator (`^uppercase_required$` , func (match []string , value * generic.Value ) error {
v := value .String ()
if v != strings .ToUpper (v ) {
return fmt .Errorf ("must be uppercase" )
}
return nil
})
}
type Config struct {
Region string `validation:"required,uppercase_required"`
}
Custom validator with parameters
// matches: divisible_by(10), divisible_by(5), etc.
validation .RegisterValidator (`^divisible_by\((\d+)\)$` , func (match []string , value * generic.Value ) error {
n , _ := strconv .Atoi (match [1 ])
if n == 0 {
return fmt .Errorf ("divisor cannot be zero" )
}
if value .Int ()% n != 0 {
return fmt .Errorf ("must be divisible by %d" , n )
}
return nil
})
type Batch struct {
Count int `validation:"divisible_by(10)"`
}
import (
"github.com/getevo/evo/v2/lib/validation"
"github.com/getevo/evo/v2/lib/db"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
validation .RegisterDBValidator (
`^active_user$` ,
func (match []string , value * generic.Value , stmt * gorm.Statement , field * schema.Field ) error {
var count int64
db .Table ("users" ).Where ("id = ? AND active = ?" , value .Input , true ).Count (& count )
if count == 0 {
return fmt .Errorf ("user must be active" )
}
return nil
},
)
type Post struct {
AuthorID uint `validation:"required,fk,active_user"`
}
Errors are returned as []error. Each message includes the field name (from the json tag or struct field name) and the reason:
email invalid email
password password must contain at least 3 of: uppercase, lowercase, digits, symbols
age is smaller than or equal to 18
tags must have at least 1 items
package main
import (
"context"
"fmt"
"time"
"github.com/getevo/evo/v2/lib/validation"
)
type RegisterRequest struct {
Username string `json:"username" validation:"required,slug,len>=3,len<=30"`
Email string `json:"email" validation:"required,email,unique"`
Password string `json:"password" validation:"required,password(medium)"`
PasswordConfirmation string `json:"password_confirmation"`
Age int `json:"age" validation:"required,>=13,<=120"`
Country string `json:"country" validation:"required,country_alpha2"`
Website string `json:"website" validation:"url"`
Tags []string `json:"tags" validation:"max_items(5),unique_items"`
BirthDate string `json:"birth_date" validation:"date,before_now"`
}
func main () {
req := RegisterRequest {
Username : "alice_dev" ,
Email : "alice@example.com" ,
Password : "MyPass1!" ,
PasswordConfirmation : "MyPass1!" ,
Age : 25 ,
Country : "US" ,
Website : "https://alice.dev" ,
Tags : []string {"go" , "backend" },
BirthDate : "1999-06-15T00:00:00Z" ,
}
if errs := validation .Struct (req ); len (errs ) > 0 {
for _ , e := range errs {
fmt .Println ("Error:" , e )
}
return
}
fmt .Println ("Validation passed!" )
// Partial update — only validate non-zero fields
partial := RegisterRequest {Email : "new@example.com" }
if errs := validation .StructNonZeroFields (partial ); len (errs ) > 0 {
for _ , e := range errs {
fmt .Println ("Error:" , e )
}
}
// Single value
if err := validation .Value ("+15551234567" , "e164" ); err != nil {
fmt .Println (err )
}
// With context timeout (for DB validators)
ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
defer cancel ()
errs := validation .StructWithContext (ctx , req )
_ = errs
}