validator.go
April 14, 2026 · View on GitHub
A Go validation library with composable rules, nested struct support, and detailed error reporting.
Installation
go get github.com/raoptimus/validator.go/v2
Usage
Validate a single value
import validator "github.com/raoptimus/validator.go/v2"
ctx := context.Background()
err := validator.ValidateValue(ctx, "user@example.com",
validator.NewRequired(),
validator.NewEmail(),
)
// err == nil
Validate a struct
Field names in RuleSet must match struct field names. Error paths use json tags as aliases.
type SignUpRequest struct {
Email string `json:"email"`
Name string `json:"name"`
Age int `json:"age"`
}
req := SignUpRequest{
Email: "bad-email",
Name: "",
Age: 200,
}
err := validator.Validate(ctx, &req, validator.RuleSet{
"Email": {validator.NewRequired(), validator.NewEmail()},
"Name": {validator.NewRequired(), validator.NewStringLength(1, 100)},
"Age": {validator.NewRequired(), validator.NewNumber(1, 150)},
})
// err.Error() == "email: Email is not a valid email. name: Value cannot be blank. age: Value must be no greater than 150."
Validate a map
data := map[string]any{
"count": 0,
}
err := validator.Validate(ctx, data, validator.RuleSet{
"count": {
validator.NewRequired(),
validator.NewNumber(1, 100),
},
})
Nested structs
type Address struct {
City string `json:"city"`
Street string `json:"street"`
}
type User struct {
Name string `json:"name"`
Address Address `json:"address"`
}
err := validator.Validate(ctx, &User{Name: "Alice"}, validator.RuleSet{
"Name": {validator.NewRequired()},
"Address": {
validator.NewNested(validator.RuleSet{
"City": {validator.NewRequired()},
"Street": {validator.NewRequired(), validator.NewStringLength(1, 255)},
}),
},
})
// Errors have dotted paths: "address.city", "address.street"
Validate each element in a slice
err := validator.ValidateValue(ctx, []string{"ok", "", "fine"},
validator.NewEach(validator.NewStringLength(1, 255)),
)
// Error path: "1" (index of the empty string)
Conditional validation
validator.NewEmail().When(func(ctx context.Context, value any) bool {
return value != nil && value.(string) != ""
})
Skip on empty / skip on error
// Skip email format check if the value is empty
validator.NewEmail().SkipOnEmpty()
// Skip this rule if a previous rule already failed
validator.NewStringLength(1, 100).SkipOnError()
OR rule
At least one of the rules must pass:
validator.NewOR("Must be a valid email or URL",
validator.NewEmail(),
validator.NewURL(),
)
Custom callback
validator.NewCallback(func(ctx context.Context, value string) error {
if strings.Contains(value, "forbidden") {
return validator.NewValidationError("Value contains forbidden word.")
}
return nil
})
Error handling
err := validator.Validate(ctx, &req, rules)
if err != nil {
var result validator.Result
if errors.As(err, &result) {
// Map of field path -> error messages
errorMap := result.ErrorMessagesIndexedByPath()
// e.g. {"email": ["Email is not a valid email."], "age": ["Value must be no greater than 150."]}
}
}
Multilanguage support
Validation messages are translated automatically based on the language in context.
Built-in languages: English (en, default) and Russian (ru).
// Set language in context
ctx := validator.WithLanguage(context.Background(), validator.LanguageRU)
err := validator.ValidateValue(ctx, "", validator.NewRequired())
// err.Error() == "Значение не должно быть пустым."
HTTP middleware example
func LangMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lang := r.Header.Get("Accept-Language") // "ru", "en", etc.
ctx := validator.WithLanguage(r.Context(), validator.Language(lang))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Now any validator call inside the handler will use the language from the request:
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
var req CreateRequest
// ... decode request body ...
// language is already in r.Context() from the middleware
err := validator.Validate(r.Context(), &req, validator.RuleSet{
"Name": {validator.NewRequired()},
})
// Accept-Language: ru → "Значение не должно быть пустым."
// Accept-Language: en → "Value cannot be blank."
}
Register a custom language
func init() {
validator.Translations.Register(validator.Language("es"), map[string]string{
validator.MessageRequired: "El valor no puede estar vacío.",
validator.MessageTooShort: "El valor debe contener al menos {min} caracteres.",
// ...
})
}
Check for missing translations
missing := validator.Translations.Missing(validator.Language("es"))
// returns a list of message IDs without translation for the given language
Use DummyTranslator (no translation, placeholder replacement only)
validator.SetTranslator(&validator.DummyTranslator{})
Available Rules
| Rule | Description |
|---|---|
Required | Value must not be empty |
Number | Integer within min/max range |
StringLength | UTF-8 string length within min/max |
Email | Valid email format |
URL | Valid URL format |
IP | Valid IP address |
UUID | Valid UUID format |
MAC | Valid MAC address |
MSISDN | Valid mobile phone number |
OGRN | Valid Russian OGRN number |
JSON | Valid JSON structure |
SQL | Valid SQL statement |
MatchRegularExpression | Matches a custom regex |
In | Value is in a predefined set |
InRange | Numeric value within a range |
Compare | Compare against another value (==, !=, <, >, <=, >=) |
Nested | Validate nested struct fields |
Each | Validate each element in a slice/array |
OR | At least one rule must pass |
Callback | Custom validation function |
UniqueValues | All elements in a slice are unique |
NumericString | String is a valid numeric representation |
HumanText | Valid human-readable text |
ImageMeta | Image metadata validation |
Time | Time format and range validation |