validation API

validation

package

API reference for the validation package.

T
type

Rule

Rule validates a field value given an optional parameter.

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

compiledField

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

Fields

Name Type Description
Index int
Rules []compiledRule
S
struct

compiledRule

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

Fields

Name Type Description
Name string
Rule Rule
Param string
S
struct

typeCache

core/validation/validator.go:28-31
type typeCache struct

Fields

Name Type Description
fields []compiledField
errors Errors
S
struct

Validator

Validator validates structs based on “validate” struct tags.

core/validation/validator.go:34-42
type Validator struct

Methods

Register
Method

Parameters

name string
rule Rule

Returns

error
func (*Validator) Register(name string, rule Rule) error
{
	if strings.TrimSpace(name) == "" {
		return fmt.Errorf("validation: rule name cannot be empty")
	}
	if rule == nil {
		return fmt.Errorf("validation: rule %q cannot be nil", name)
	}
	v.mu.Lock()
	v.rules[name] = rule
	v.mu.Unlock()
	v.typeMu.Lock()
	v.typeCache = make(map[reflect.Type]*typeCache)
	v.typeMu.Unlock()
	return nil
}
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()

	errs := append(Errors(nil), tc.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
			}
			rule, ok := v.rules[ruleName]
			if !ok {
				tc.errors = append(tc.errors, Error{
					Field:   meta.Name,
					Message: fmt.Sprintf("unknown validation rule %q", ruleName),
				})
				continue
			}
			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 fmt.Errorf("invalid min parameter %q", param)
			}
			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 fmt.Errorf("invalid min parameter %q", param)
			}
			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 fmt.Errorf("invalid max parameter %q", param)
			}
			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 fmt.Errorf("invalid max parameter %q", param)
			}
			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 fmt.Errorf("invalid pattern: %w", err)
				}
				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.

core/validation/validator.go:45-48
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.

core/validation/validator.go:55-55
type Errors []Error
F
function

New

New creates a new Validator with built-in rules.

Returns

core/validation/validator.go:66-75
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

core/validation/validator_test.go:9-14
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

core/validation/validator_test.go:16-18
type optionalInt struct

Fields

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

TestValidator_RequiredString

Parameters

core/validation/validator_test.go:20-36
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

core/validation/validator_test.go:38-51
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

core/validation/validator_test.go:53-66
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

core/validation/validator_test.go:68-81
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

core/validation/validator_test.go:83-96
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

core/validation/validator_test.go:98-104
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

core/validation/validator_test.go:106-112
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

core/validation/validator_test.go:114-120
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

core/validation/validator_test.go:122-127
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

core/validation/validator_test.go:129-135
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")
	}
}
F
function

TestValidatorRejectsUnknownRule

Parameters

core/validation/validator_test.go:137-145
func TestValidatorRejectsUnknownRule(t *testing.T)

{
	type target struct {
		Value string `validate:"misspelled"`
	}
	errs := New().Validate(target{Value: "anything"})
	if len(errs) != 1 || !strings.Contains(errs[0].Message, "unknown validation rule") {
		t.Fatalf("Validate() = %v, want unknown rule error", errs)
	}
}
F
function

TestValidatorRejectsInvalidRuleParameters

Parameters

core/validation/validator_test.go:147-156
func TestValidatorRejectsInvalidRuleParameters(t *testing.T)

{
	type target struct {
		Count int    `validate:"min=wrong"`
		Value string `validate:"pattern=["`
	}
	errs := New().Validate(target{})
	if len(errs) != 2 {
		t.Fatalf("Validate() errors = %v, want two configuration errors", errs)
	}
}
F
function

TestRegisterInvalidatesCompiledTypes

Parameters

core/validation/validator_test.go:158-172
func TestRegisterInvalidatesCompiledTypes(t *testing.T)

{
	type target struct {
		Value string `validate:"custom"`
	}
	validator := New()
	if len(validator.Validate(target{})) != 1 {
		t.Fatal("Validate() did not reject unknown rule")
	}
	if err := validator.Register("custom", func(reflect.Value, string) error { return nil }); err != nil {
		t.Fatal(err)
	}
	if errs := validator.Validate(target{}); len(errs) != 0 {
		t.Fatalf("Validate() retained stale unknown rule: %v", errs)
	}
}