validation API

validation

package

API reference for the validation package.

T
type

Rule

Rule validates a field value given an optional parameter.

pkg/validation/validator.go:15-15
type Rule func(value reflect.Value, param string) error
S
struct

compiledField

pkg/validation/validator.go:17-20
type compiledField struct

Fields

Name Type Description
Index int
Rules []compiledRule
S
struct

compiledRule

pkg/validation/validator.go:22-26
type compiledRule struct

Fields

Name Type Description
Name string
Rule Rule
Param string
S
struct

typeCache

pkg/validation/validator.go:28-30
type typeCache struct

Fields

Name Type Description
fields []compiledField
S
struct

Validator

Validator validates structs based on “validate” struct tags.

pkg/validation/validator.go:33-41
type Validator struct

Methods

Register
Method

Parameters

name string
rule Rule
func (*Validator) Register(name string, rule Rule)
{
	v.mu.Lock()
	defer v.mu.Unlock()
	v.rules[name] = rule
}
Validate
Method

Parameters

target any

Returns

func (*Validator) Validate(target any) Errors
{
	val := reflect.ValueOf(target)
	if val.Kind() == reflect.Ptr {
		val = val.Elem()
	}
	if val.Kind() != reflect.Struct {
		return Errors{{Field: "", Message: "target must be a struct"}}
	}

	typ := val.Type()
	v.typeMu.RLock()
	tc, ok := v.typeCache[typ]
	v.typeMu.RUnlock()

	if !ok {
		v.typeMu.Lock()
		tc, ok = v.typeCache[typ]
		if !ok {
			tc = v.compileType(typ, target)
			v.typeCache[typ] = tc
		}
		v.typeMu.Unlock()
	}

	v.mu.RLock()
	defer v.mu.RUnlock()

	var errs Errors
	for _, cf := range tc.fields {
		fieldVal := val.Field(cf.Index)
		for _, cr := range cf.Rules {
			if err := cr.Rule(fieldVal, cr.Param); err != nil {
				errs = append(errs, Error{
					Field:   typ.Field(cf.Index).Name,
					Message: err.Error(),
				})
			}
		}
	}
	return errs
}
compileType
Method

Parameters

target any

Returns

func (*Validator) compileType(typ reflect.Type, target any) *typeCache
{
	fields := tagParser.ParseStruct(target)
	v.mu.RLock()
	defer v.mu.RUnlock()

	tc := &typeCache{}
	for _, meta := range fields {
		tag := meta.RawTag
		if tag == "" {
			continue
		}
		cf := compiledField{Index: meta.Index}
		rules := strings.Split(tag, ",")
		for _, ruleDef := range rules {
			ruleDef = strings.TrimSpace(ruleDef)
			if ruleDef == "" {
				continue
			}
			var ruleName, param string
			if idx := strings.Index(ruleDef, "="); idx != -1 {
				ruleName = ruleDef[:idx]
				param = ruleDef[idx+1:]
			} else {
				ruleName = ruleDef
			}
			if rule, ok := v.rules[ruleName]; ok {
				cf.Rules = append(cf.Rules, compiledRule{Name: ruleName, Rule: rule, Param: param})
			}
		}
		if len(cf.Rules) > 0 {
			tc.fields = append(tc.fields, cf)
		}
	}
	return tc
}
func (*Validator) registerBuiltin()
{
	v.rules["required"] = func(field reflect.Value, _ string) error {
		switch field.Kind() {
		case reflect.String:
			if field.String() == "" {
				return fmt.Errorf("required")
			}
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		case reflect.Float32, reflect.Float64:
		case reflect.Slice, reflect.Map:
			if field.Len() == 0 {
				return fmt.Errorf("required")
			}
		case reflect.Bool:
		default:
			if field.IsZero() {
				return fmt.Errorf("required")
			}
		}
		return nil
	}

	v.rules["min"] = func(field reflect.Value, param string) error {
		switch field.Kind() {
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			min, err := strconv.ParseInt(param, 10, 64)
			if err != nil {
				return nil
			}
			if field.Int() < min {
				return fmt.Errorf("min %v", min)
			}
		case reflect.Float32, reflect.Float64:
			min, err := strconv.ParseFloat(param, 64)
			if err != nil {
				return nil
			}
			if field.Float() < min {
				return fmt.Errorf("min %v", min)
			}
		}
		return nil
	}

	v.rules["max"] = func(field reflect.Value, param string) error {
		switch field.Kind() {
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			max, err := strconv.ParseInt(param, 10, 64)
			if err != nil {
				return nil
			}
			if field.Int() > max {
				return fmt.Errorf("max %v", max)
			}
		case reflect.Float32, reflect.Float64:
			max, err := strconv.ParseFloat(param, 64)
			if err != nil {
				return nil
			}
			if field.Float() > max {
				return fmt.Errorf("max %v", max)
			}
		}
		return nil
	}

	v.rules["email"] = func(field reflect.Value, _ string) error {
		if field.Kind() != reflect.String {
			return nil
		}
		if !v.emailRegex.MatchString(field.String()) {
			return fmt.Errorf("invalid email")
		}
		return nil
	}

	v.rules["pattern"] = func(field reflect.Value, param string) error {
		if field.Kind() != reflect.String {
			return nil
		}
		v.cacheMu.RLock()
		re, ok := v.cache[param]
		v.cacheMu.RUnlock()
		if !ok {
			v.cacheMu.Lock()
			re, ok = v.cache[param]
			if !ok {
				var err error
				re, err = regexp.Compile(param)
				if err != nil {
					v.cacheMu.Unlock()
					return nil
				}
				v.cache[param] = re
			}
			v.cacheMu.Unlock()
		}
		if !re.MatchString(field.String()) {
			return fmt.Errorf("pattern mismatch")
		}
		return nil
	}
}

Fields

Name Type Description
mu sync.RWMutex
rules map[string]Rule
emailRegex *regexp.Regexp
cache map[string]*regexp.Regexp
cacheMu sync.RWMutex
typeCache map[reflect.Type]*typeCache
typeMu sync.RWMutex
S
struct

Error

Error represents a single field validation error.

pkg/validation/validator.go:44-47
type Error struct

Methods

Error
Method

Returns

string
func (Error) Error() string
{
	return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

Fields

Name Type Description
Field string json:"field"
Message string json:"message"
T
type

Errors

Errors is a collection of validation errors.

pkg/validation/validator.go:54-54
type Errors []Error
F
function

New

New creates a new Validator with built-in rules.

Returns

pkg/validation/validator.go:65-74
func New() *Validator

{
	v := &Validator{
		rules:      make(map[string]Rule),
		emailRegex: regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`),
		cache:      make(map[string]*regexp.Regexp),
		typeCache:  make(map[reflect.Type]*typeCache),
	}
	v.registerBuiltin()
	return v
}
S
struct

user

pkg/validation/validator_test.go:7-12
type user struct

Fields

Name Type Description
Name string validate:"required"
Age int validate:"min=18,max=99"
Email string validate:"email"
Label string validate:"pattern=^[a-z]+$"
S
struct

optionalInt

pkg/validation/validator_test.go:14-16
type optionalInt struct

Fields

Name Type Description
Count int validate:"required"
F
function

TestValidator_RequiredString

Parameters

pkg/validation/validator_test.go:18-34
func TestValidator_RequiredString(t *testing.T)

{
	v := New()
	errs := v.Validate(user{Name: "", Email: "[email protected]"})
	if len(errs) == 0 {
		t.Error("expected error for empty Name")
	}
	found := false
	for _, e := range errs {
		if e.Field == "Name" {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected error on field Name")
	}
}
F
function

TestValidator_MinInt

Parameters

pkg/validation/validator_test.go:36-49
func TestValidator_MinInt(t *testing.T)

{
	v := New()
	errs := v.Validate(user{Name: "x", Age: 10, Email: "[email protected]"})
	found := false
	for _, e := range errs {
		if e.Field == "Age" {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected error for Age < 18")
	}
}
F
function

TestValidator_MaxInt

Parameters

pkg/validation/validator_test.go:51-64
func TestValidator_MaxInt(t *testing.T)

{
	v := New()
	errs := v.Validate(user{Name: "x", Age: 100, Email: "[email protected]"})
	found := false
	for _, e := range errs {
		if e.Field == "Age" {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected error for Age > 99")
	}
}
F
function

TestValidator_ValidEmail

Parameters

pkg/validation/validator_test.go:66-79
func TestValidator_ValidEmail(t *testing.T)

{
	v := New()
	errs := v.Validate(user{Name: "x", Age: 25, Email: "notanemail"})
	found := false
	for _, e := range errs {
		if e.Field == "Email" {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected error for invalid email")
	}
}
F
function

TestValidator_Pattern

Parameters

pkg/validation/validator_test.go:81-94
func TestValidator_Pattern(t *testing.T)

{
	v := New()
	errs := v.Validate(user{Name: "x", Age: 25, Email: "[email protected]", Label: "ABC"})
	found := false
	for _, e := range errs {
		if e.Field == "Label" {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected error for Label pattern mismatch")
	}
}
F
function

TestValidator_AllValid

Parameters

pkg/validation/validator_test.go:96-102
func TestValidator_AllValid(t *testing.T)

{
	v := New()
	errs := v.Validate(user{Name: "Alice", Age: 25, Email: "[email protected]", Label: "abc"})
	if len(errs) > 0 {
		t.Errorf("expected no errors, got %v", errs)
	}
}
F
function

TestValidator_EmptyRequired

Parameters

pkg/validation/validator_test.go:104-110
func TestValidator_EmptyRequired(t *testing.T)

{
	v := New()
	errs := v.Validate(user{Name: "", Age: 25, Email: "[email protected]"})
	if len(errs) == 0 {
		t.Error("expected error for required field")
	}
}
F
function

TestValidator_NonStruct

Parameters

pkg/validation/validator_test.go:112-118
func TestValidator_NonStruct(t *testing.T)

{
	v := New()
	errs := v.Validate(42)
	if len(errs) == 0 {
		t.Error("expected error for non-struct")
	}
}
F
function

TestValidator_ErrorsType_ErrorMethod

Parameters

pkg/validation/validator_test.go:120-125
func TestValidator_ErrorsType_ErrorMethod(t *testing.T)

{
	errs := Errors{{Field: "x", Message: "bad"}}
	if errs.Error() != "x: bad" {
		t.Errorf("got %q", errs.Error())
	}
}
F
function

TestValidator_OptionalInt

Parameters

pkg/validation/validator_test.go:127-133
func TestValidator_OptionalInt(t *testing.T)

{
	v := New()
	errs := v.Validate(optionalInt{Count: 0})
	if len(errs) > 0 {
		t.Error("zero int should be allowed for required since Go can't distinguish zero from unset")
	}
}