validator.go

April 14, 2026 · View on GitHub

CI Coverage Go Reference Go Report Card GitHub Release License

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

RuleDescription
RequiredValue must not be empty
NumberInteger within min/max range
StringLengthUTF-8 string length within min/max
EmailValid email format
URLValid URL format
IPValid IP address
UUIDValid UUID format
MACValid MAC address
MSISDNValid mobile phone number
OGRNValid Russian OGRN number
JSONValid JSON structure
SQLValid SQL statement
MatchRegularExpressionMatches a custom regex
InValue is in a predefined set
InRangeNumeric value within a range
CompareCompare against another value (==, !=, <, >, <=, >=)
NestedValidate nested struct fields
EachValidate each element in a slice/array
ORAt least one rule must pass
CallbackCustom validation function
UniqueValuesAll elements in a slice are unique
NumericStringString is a valid numeric representation
HumanTextValid human-readable text
ImageMetaImage metadata validation
TimeTime format and range validation

License

BSD 3-Clause