Repository: go-playground/validator
Pull request #752 (2021-04-15 23:23:50)

Add ability to validate map data
Add ability to validate map data based on map validation rules.
In some frameworks like kataras/iris, they return a map of form values. 
So it is hard to validate data that is not in struct form. So we can easily provide the data
itself and validation rules in form of map to have an easier data validation

@go-playground/admins

coveralls: 
[![Coverage Status](https://coveralls.io/builds/38867598/badge)](https://coveralls.io/builds/38867598)

Coverage decreased (-0.07%) to 76.022% when pulling **19e6b81c2e3cc8b77f49bc6eb9b3eaa2265ac251 on zytell3301:master** into **b926bf0df97108effd04f9325365cd7969935e16 on go-playground:master**.

deankarn: Thanks for the PR @zytell3301 

My only request would be that it support nested data. This looks great for flat data structures but not sure how you could apply validations for nested data.

Thoughts?
zytell3301: I think i've found a solution for it. I'll push the changes after I implemented it.

Thanks for your attention @deankarn and apologies for any mistakes in PR or commits.
I'm new to these stuff
zytell3301: Hi. I just implemented validating nested maps. But cross-field validation and keys validation is missing for now. Please check the updates.

Patch:
diff --git a/_examples/map-validation/main.go b/_examples/map-validation/main.go
new file mode 100644
index 0000000..92e282f
--- /dev/null
+++ b/_examples/map-validation/main.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+	"fmt"
+	"github.com/go-playground/validator/v10"
+)
+
+var validate *validator.Validate
+
+func main() {
+	validate = validator.New()
+
+	validateMap()
+	validateNestedMap()
+}
+
+func validateMap() {
+	user := map[string]interface{}{"name": "Arshiya Kiani", "email": "zytel3301@gmail.com"}
+
+	// Every rule will be applied to the item of the data that the offset of rule is pointing to.
+	// So if you have a field "email": "omitempty,required,email", the validator will apply these
+	// rules to offset of email in user data
+	rules := map[string]interface{}{"name": "required,min=8,max=32", "email": "omitempty,required,email"}
+
+	// ValidateMap will return map[string]error.
+	// The offset of every item in errs is the name of invalid field and the value
+	// is the message of error. If there was no error, ValidateMap method will
+	// return an EMPTY map of errors, not nil. If you want to check that
+	// if there was an error or not, you must check the length of the return value
+	errs := validate.ValidateMap(user, rules)
+
+	if len(errs) > 0 {
+		fmt.Println(errs)
+		// The user is invalid
+	}
+
+	// The user is valid
+}
+
+func validateNestedMap() {
+
+	data := map[string]interface{}{
+		"name":  "Arshiya Kiani",
+		"email": "zytel3301@gmail.com",
+		"details": map[string]interface{}{
+			"family_members": map[string]interface{}{
+				"father_name": "Micheal",
+				"mother_name": "Hannah",
+			},
+			"salary": "1000",
+		},
+	}
+
+	// Rules must be set as the structure as the data itself. If you want to dive into the
+	// map, just declare its rules as a map
+	rules := map[string]interface{}{
+		"name":  "min=4,max=32",
+		"email": "required,email",
+		"details": map[string]interface{}{
+			"family_members": map[string]interface{}{
+				"father_name": "required,min=4,max=32",
+				"mother_name": "required,min=4,max=32",
+			},
+			"salary": "number",
+		},
+	}
+
+	if len(validate.ValidateMap(data, rules)) == 0 {
+		// Data is valid
+	}
+
+	// Data is invalid
+}
diff --git a/baked_in.go b/baked_in.go
index dd25705..78af882 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -1038,6 +1038,9 @@ func isNeCrossStructField(fl FieldLevel) bool {
 	case reflect.Slice, reflect.Map, reflect.Array:
 		return int64(topField.Len()) != int64(field.Len())
 
+	case reflect.Bool:
+		return topField.Bool() != field.Bool()
+
 	case reflect.Struct:
 
 		fieldType := field.Type()
@@ -1085,6 +1088,9 @@ func isEqCrossStructField(fl FieldLevel) bool {
 	case reflect.Slice, reflect.Map, reflect.Array:
 		return int64(topField.Len()) == int64(field.Len())
 
+	case reflect.Bool:
+		return topField.Bool() == field.Bool()
+
 	case reflect.Struct:
 
 		fieldType := field.Type()
@@ -1132,6 +1138,9 @@ func isEqField(fl FieldLevel) bool {
 	case reflect.Slice, reflect.Map, reflect.Array:
 		return int64(field.Len()) == int64(currentField.Len())
 
+	case reflect.Bool:
+		return field.Bool() == currentField.Bool()
+
 	case reflect.Struct:
 
 		fieldType := field.Type()
@@ -1446,6 +1455,9 @@ func requireCheckFieldValue(fl FieldLevel, param string, value string, defaultNo
 
 	case reflect.Slice, reflect.Map, reflect.Array:
 		return int64(field.Len()) == asInt(value)
+
+	case reflect.Bool:
+		return field.Bool() == asBool(value)
 	}
 
 	// default reflect.String:
diff --git a/validator_instance.go b/validator_instance.go
index d230a32..8e27707 100644
--- a/validator_instance.go
+++ b/validator_instance.go
@@ -143,6 +143,33 @@ func (v *Validate) SetTagName(name string) {
 	v.tagName = name
 }
 
+// ValidateMapCtx validates a map using a map of validation rules and allows passing of contextual
+// validation validation information via context.Context.
+func (v Validate) ValidateMapCtx(ctx context.Context, data map[string]interface{}, rules map[string]interface{}) map[string]interface{} {
+	errs := make(map[string]interface{})
+	for field, rule := range rules {
+		if reflect.ValueOf(rule).Kind() == reflect.Map && reflect.ValueOf(data[field]).Kind() == reflect.Map {
+			err := v.ValidateMapCtx(ctx, data[field].(map[string]interface{}), rule.(map[string]interface{}))
+			if len(err) > 0 {
+				errs[field] = err
+			}
+		} else if reflect.ValueOf(rule).Kind() == reflect.Map {
+			errs[field] = errors.New("The field: '" + field + "' is not a map to dive")
+		} else {
+			err := v.VarCtx(ctx, data[field], rule.(string))
+			if err != nil {
+				errs[field] = err
+			}
+		}
+	}
+	return errs
+}
+
+// ValidateMap validates map data form a map of tags
+func (v *Validate) ValidateMap(data map[string]interface{}, rules map[string]interface{}) map[string]interface{} {
+	return v.ValidateMapCtx(context.Background(), data, rules)
+}
+
 // RegisterTagNameFunc registers a function to get alternate names for StructFields.
 //
 // eg. to use the names which have been specified for JSON representations of structs, rather than normal Go field names:


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 1b2e39b..34232c6 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -1670,12 +1670,14 @@ func TestCrossStructNeFieldValidation(t *testing.T) {
 	i := 1
 	j = 1
 	k = 1.543
+	b := true
 	arr := []string{"test"}
 
 	s2 := "abcd"
 	i2 := 1
 	j2 = 1
 	k2 = 1.543
+	b2 := true
 	arr2 := []string{"test"}
 	arr3 := []string{"test", "test2"}
 	now2 := now
@@ -1696,6 +1698,10 @@ func TestCrossStructNeFieldValidation(t *testing.T) {
 	NotEqual(t, errs, nil)
 	AssertError(t, errs, "", "", "", "", "necsfield")
 
+	errs = validate.VarWithValue(b2, b, "necsfield")
+	NotEqual(t, errs, nil)
+	AssertError(t, errs, "", "", "", "", "necsfield")
+
 	errs = validate.VarWithValue(arr2, arr, "necsfield")
 	NotEqual(t, errs, nil)
 	AssertError(t, errs, "", "", "", "", "necsfield")
@@ -1834,6 +1840,7 @@ func TestCrossStructEqFieldValidation(t *testing.T) {
 	i := 1
 	j = 1
 	k = 1.543
+	b := true
 	arr := []string{"test"}
 
 	var j2 uint64
@@ -1842,6 +1849,7 @@ func TestCrossStructEqFieldValidation(t *testing.T) {
 	i2 := 1
 	j2 = 1
 	k2 = 1.543
+	b2 := true
 	arr2 := []string{"test"}
 	arr3 := []string{"test", "test2"}
 	now2 := now
@@ -1858,6 +1866,9 @@ func TestCrossStructEqFieldValidation(t *testing.T) {
 	errs = validate.VarWithValue(k2, k, "eqcsfield")
 	Equal(t, errs, nil)
 
+	errs = validate.VarWithValue(b2, b, "eqcsfield")
+	Equal(t, errs, nil)
+
 	errs = validate.VarWithValue(arr2, arr, "eqcsfield")
 	Equal(t, errs, nil)
 
@@ -4829,6 +4840,7 @@ func TestIsEqFieldValidation(t *testing.T) {
 	i := 1
 	j = 1
 	k = 1.543
+	b := true
 	arr := []string{"test"}
 	now := time.Now().UTC()
 
@@ -4838,6 +4850,7 @@ func TestIsEqFieldValidation(t *testing.T) {
 	i2 := 1
 	j2 = 1
 	k2 = 1.543
+	b2 := true
 	arr2 := []string{"test"}
 	arr3 := []string{"test", "test2"}
 	now2 := now
@@ -4854,6 +4867,9 @@ func TestIsEqFieldValidation(t *testing.T) {
 	errs = validate.VarWithValue(k2, k, "eqfield")
 	Equal(t, errs, nil)
 
+	errs = validate.VarWithValue(b2, b, "eqfield")
+	Equal(t, errs, nil)
+
 	errs = validate.VarWithValue(arr2, arr, "eqfield")
 	Equal(t, errs, nil)
 
@@ -10065,12 +10081,15 @@ func TestRequiredUnless(t *testing.T) {
 		Field6  uint              `validate:"required_unless=Field5 2" json:"field_6"`
 		Field7  float32           `validate:"required_unless=Field6 0" json:"field_7"`
 		Field8  float64           `validate:"required_unless=Field7 0.0" json:"field_8"`
+		Field9  bool              `validate:"omitempty" json:"field_9"`
+		Field10 string            `validate:"required_unless=Field9 true" json:"field_10"`
 	}{
 		FieldE: "test",
 		Field2: &fieldVal,
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
 		Field5: 2,
+		Field9: true,
 	}
 
 	validate := New()
@@ -10090,6 +10109,8 @@ func TestRequiredUnless(t *testing.T) {
 		Field5  string            `validate:"required_unless=Field3 0" json:"field_5"`
 		Field6  string            `validate:"required_unless=Inner.Field test" json:"field_6"`
 		Field7  string            `validate:"required_unless=Inner2.Field test" json:"field_7"`
+		Field8  bool              `validate:"omitempty" json:"field_8"`
+		Field9  string            `validate:"required_unless=Field8 true" json:"field_9"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		FieldE: "test",
@@ -10100,10 +10121,11 @@ func TestRequiredUnless(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 3)
+	Equal(t, len(ve), 4)
 	AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_unless")
 	AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_unless")
 	AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_unless")
+	AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_unless")
 
 	defer func() {
 		if r := recover(); r == nil {