Skip to content

Cross-field & Business Rules

Tag-based rules cover single-field constraints. For anything that depends on two or more fields (password confirmation, conditional required, computed bounds…) use the Validate() error hook.

The contract

If the struct value (or its pointer) implements:

go
Validate() error

…govalid invokes it after the tag rules run, on every Check call. Any non-nil error is appended to the result slice as an *ErrContext — wrapping the message you returned.

The signature must be exactly func() error:

Method shapeCalled?
func (T) Validate() erroryes (value receiver)
func (*T) Validate() erroryes when caller passes *T
func (T) Validate(extra string) errorno — wrong arity
func (T) Validate() stringno — wrong return

Password confirmation

go
type Form struct {
    Password       string `valid:"required;minlen:8" label:"密码"`
    RepeatPassword string `valid:"required"          label:"重复密码"`
}

func (f *Form) Validate() error {
    if f.Password != f.RepeatPassword {
        return errors.New("两次输入的密码不一致")
    }
    return nil
}

Equivalent with the built-in equal checker:

go
type Form struct {
    Password       string `valid:"required;minlen:8" label:"密码"`
    RepeatPassword string `valid:"required;equal:Password" label:"重复密码"`
}

The Validate form is more flexible — you can mix conditions and external lookups.

Conditional required

go
type Address struct {
    Country string `valid:"required;list:CN,US"`
    State   string // required only when Country == "US"
}

func (a Address) Validate() error {
    if a.Country == "US" && a.State == "" {
        return errors.New("state is required for US addresses")
    }
    return nil
}

External lookups

Because Validate is plain Go, you can call out to a database, cache, or any other service. Just don't make Check your authorization boundary — it has no notion of cancellation or timeouts.

go
type CreateUser struct {
    Username string `valid:"required;username"`
    Email    string `valid:"required;email"`
}

func (u *CreateUser) Validate() error {
    if exists, _ := userRepo.UsernameExists(u.Username); exists {
        return fmt.Errorf("用户名 %q 已被占用", u.Username)
    }
    return nil
}

TIP

Keep Validate deterministic and side-effect-free where possible. Validation is often called more than once per request (preview, save, re-render) — surprise side effects make debugging hard.

Returning a richer error

Validate may return any error. govalid stringifies it via Error() when building the user-facing message, so a custom error type whose Error() returns a localized string works seamlessly:

go
type LocalizedErr struct{ zh, en string }

func (e *LocalizedErr) Error() string { return e.zh }

func (f *Form) Validate() error {
    if !ok {
        return &LocalizedErr{zh: "操作失败", en: "operation failed"}
    }
    return nil
}

If you need access to the original error type, type-assert through errors.As against the error value returned by errs[i].Unwrap() — note that *ErrContext does not currently expose Unwrap, so for now it's simplest to keep your error data in Error().

Released under the MIT License.