Skip to content

Custom Checkers

Need something the built-ins don't cover? Register your own checker by adding a function to the govalid.Checkers map. Once registered, the new rule is usable from any valid tag — no other plumbing required.

The simplest case

go
package main

import (
    "fmt"
    "strings"

    "github.com/wuhan005/govalid"
)

func main() {
    govalid.SetMessageTemplates(map[string]string{
        "noE99": "can not contain 'e99'",
    })

    govalid.Checkers["noE99"] = func(c govalid.CheckerContext) *govalid.ErrContext {
        v, ok := c.FieldValue.(string)
        if !ok {
            return govalid.MakeValueTypeError(c)
        }
        if strings.Contains(v, "e99") {
            return govalid.NewErrorContext(c)
        }
        return nil
    }

    r := struct {
        Content string `valid:"noE99" label:"内容"`
    }{Content: "helloe99"}

    if errs, ok := govalid.Check(r); !ok {
        for _, err := range errs {
            fmt.Println(err)
        }
    }
}
text
内容can not contain 'e99'

The contract

A checker is:

go
type CheckFunc func(ctx CheckerContext) *ErrContext

Return nil on success and a non-nil *ErrContext on failure.

The CheckerContext carries everything you need:

FieldWhat it gives you
FieldValuethe actual value (you'll usually type-assert it)
FieldTypereflect.Type of the field
FieldNameGo field name
FieldLabelresolved label (locale-aware)
StructValueparent struct as reflect.Value (for cross-field rules)
Rule.paramsparameters from the tag, e.g. ["a","b","c"] for list:a,b,c
TemplateLanguageactive locale

Error helpers

Don't build *ErrContext by hand. Use these helpers:

HelperWhen to use
NewErrorContext(c)Standard rule failure — uses your registered template.
MakeValueTypeError(c)Field's runtime kind isn't supported.
MakeCheckerParamError(c)Tag parameters are missing or malformed.
MakeFieldNotFoundError(c)Cross-field rule referenced an unknown field.

Using parameters

go
const checker = "startsWith"

govalid.SetMessageTemplates(map[string]string{
    checker: "必须以指定前缀开头",
})

govalid.Checkers[checker] = func(c govalid.CheckerContext) *govalid.ErrContext {
    if len(c.Rule.params) == 0 {
        return govalid.MakeCheckerParamError(c)
    }
    v, ok := c.FieldValue.(string)
    if !ok {
        return govalid.MakeValueTypeError(c)
    }
    for _, p := range c.Rule.params {
        if strings.HasPrefix(v, p) {
            return nil
        }
    }
    return govalid.NewErrorContext(c)
}
go
type Form struct {
    Path string `valid:"startsWith:/api,/admin"`
}

Looking at sibling fields

Use c.StructValue to read other fields by name. The built-in equal checker is a good template:

go
govalid.Checkers["different"] = func(c govalid.CheckerContext) *govalid.ErrContext {
    if len(c.Rule.params) != 1 {
        return govalid.MakeCheckerParamError(c)
    }
    if !c.StructValue.IsValid() || c.StructValue.Kind() != reflect.Struct {
        return govalid.MakeFieldNotFoundError(c)
    }

    other := c.Rule.params[0]
    structType := c.StructValue.Type()
    for i := 0; i < structType.NumField(); i++ {
        if structType.Field(i).Name == other {
            if fmt.Sprintf("%v", c.FieldValue) == fmt.Sprintf("%v", c.StructValue.Field(i).Interface()) {
                return govalid.NewErrorContext(c)
            }
            return nil
        }
    }
    return govalid.MakeFieldNotFoundError(c)
}

Supplying a localized message

Every built-in checker has an entry in the locale templates. Add yours with SetMessageTemplates for each locale you support:

go
govalid.SetMessageTemplates(map[string]string{
    "noE99": "can not contain 'e99'",
}, language.English)

govalid.SetMessageTemplates(map[string]string{
    "noE99": "不能包含 e99",
}, language.Chinese)

TIP

The template string is concatenated with the field label. Templates that start with {{ and end with }} skip the label entirely — useful for system-style messages like _unknownErrorTemplate.

Registering at startup

The Checkers map is not synchronized for concurrent writes. Mutate it once at program startup, then leave it alone while requests are flying:

go
func init() {
    govalid.Checkers["noE99"] = noE99Checker
}

Reading the map is safe for concurrent use as long as no writer is active at the same time, which is the typical Go map contract.

Released under the MIT License.