diff --git a/README.md b/README.md index cad746d62c..3c4fc60625 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [Controlling Log output coloring](#controlling-log-output-coloring) - [Model binding and validation](#model-binding-and-validation) - [Custom Validators](#custom-validators) + - [Custom Map and Slice Validator Tags](#custom-map-and-slice-validator-tags) - [Only Bind Query String](#only-bind-query-string) - [Bind Query String or Post Data](#bind-query-string-or-post-data) - [Bind Uri](#bind-uri) @@ -838,6 +839,46 @@ $ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10" [Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way. See the [struct-lvl-validation example](https://github.com/gin-gonic/examples/tree/master/struct-lvl-validations) to learn more. +### Custom Map and Slice Validator Tags + +It is possible to register validator tags for custom map and slice types. + +```go +package main + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +type Person struct { + FirstName string `json:"firstName" binding:"required,lte=64"` + LastName string `json:"lastName" binding:"required,lte=64"` +} + +type Managers map[string]Person + +func main() { + route := gin.Default() + + binding.RegisterValidatorTag("dive,keys,oneof=accounting finance operations,endkeys", Managers{}) + + route.POST("/managers", configureManagers) + route.Run(":8085") +} + +func configureManagers(c *gin.Context) { + var m Managers + if err := c.ShouldBindJSON(&m); err == nil { + c.JSON(http.StatusOK, gin.H{"message": "Manager configuration is valid!"}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} +``` + ### Only Bind Query String `ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). diff --git a/binding/default_validator.go b/binding/default_validator.go index 10e9bb122b..b60e3cf685 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -12,6 +12,33 @@ import ( "github.com/go-playground/validator/v10" ) +var validatorTags = make(map[reflect.Type]string) + +// RegisterValidatorTag registers a validator tag against a number of types. +// This allows defining validation for custom slice, array, and map types. For example: +// type CustomMap map[int]string +// ... +// binding.RegisterValidatorTag("gt=0", CustomMap{}) +// +// Do not use the "dive" tag (unless in conjunction with "keys"/"endkeys"). +// Slice/array/map elements are validated independently. +// +// This function will not have any effect is binding.Validator has been replaced. +// +// NOTE: This function is not thread-safe. It is intended that these all be registered prior to any validation. +func RegisterValidatorTag(tag string, types ...interface{}) { + for _, typ := range types { + t := reflect.TypeOf(typ) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Slice && t.Kind() != reflect.Array && t.Kind() != reflect.Map { + panic("validator tags can be registered only for slices, arrays, and maps") + } + validatorTags[t] = tag + } +} + type defaultValidator struct { once sync.Once validate *validator.Validate @@ -81,6 +108,13 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error { return v.validateStruct(obj) case reflect.Slice, reflect.Array: var errs validator.ValidationErrors + + if tag, ok := validatorTags[value.Type()]; ok { + if err := v.validateVar(obj, tag); err != nil { + errs = append(errs, err.(validator.ValidationErrors)...) // nolint: errorlint + } + } + count := value.Len() for i := 0; i < count; i++ { if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { @@ -89,12 +123,20 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error { } } } + if len(errs) > 0 { return errs } return nil case reflect.Map: var errs validator.ValidationErrors + + if tag, ok := validatorTags[value.Type()]; ok { + if err := v.validateVar(obj, tag); err != nil { + errs = append(errs, err.(validator.ValidationErrors)...) // nolint: errorlint + } + } + for _, key := range value.MapKeys() { if err := v.ValidateStruct(value.MapIndex(key).Interface()); err != nil { for _, fieldError := range err.(validator.ValidationErrors) { // nolint: errorlint @@ -117,6 +159,12 @@ func (v *defaultValidator) validateStruct(obj interface{}) error { return v.validate.Struct(obj) } +// validateStruct receives slice, array, and map types +func (v *defaultValidator) validateVar(obj interface{}, tag string) error { + v.lazyinit() + return v.validate.Var(obj, tag) +} + // Engine returns the underlying validator engine which powers the default // Validator instance. This is useful if you want to register custom validations // or struct level validations. See validator GoDoc for more info - diff --git a/binding/default_validator_test.go b/binding/default_validator_test.go index 51139fe140..cd502d9e11 100644 --- a/binding/default_validator_test.go +++ b/binding/default_validator_test.go @@ -96,3 +96,116 @@ func TestDefaultValidator(t *testing.T) { }) } } + +func TestRegisterValidatorTag(t *testing.T) { + type CustomSlice []struct { + A string + } + type CustomArray [10]struct { + A string + } + type CustomMap map[string]struct { + A string + } + type CustomStruct struct { + A string + } + type CustomInt int + + // only slice, array, and map types are accepted + RegisterValidatorTag("gt=0", CustomSlice{}) + RegisterValidatorTag("gt=0", &CustomSlice{}) + RegisterValidatorTag("gt=0", CustomArray{}) + RegisterValidatorTag("gt=0", &CustomArray{}) + RegisterValidatorTag("gt=0", CustomMap{}) + RegisterValidatorTag("gt=0", &CustomMap{}) + assert.Panics(t, func() { RegisterValidatorTag("gt=0", CustomStruct{}) }) + assert.Panics(t, func() { RegisterValidatorTag("gt=0", &CustomStruct{}) }) + assert.Panics(t, func() { var i CustomInt; RegisterValidatorTag("gt=0", i) }) + assert.Panics(t, func() { var i CustomInt; RegisterValidatorTag("gt=0", &i) }) +} + +func TestValidatorTagsSlice(t *testing.T) { + type CustomSlice []struct { + A string `binding:"max=8"` + } + + var ( + invalidSlice = CustomSlice{{"12345678"}} + invalidVal = CustomSlice{{"123456789"}, {"abcdefgh"}} + validSlice = CustomSlice{{"12345678"}, {"abcdefgh"}} + invalidSliceVal = CustomSlice{{"123456789"}} + ) + + v := &defaultValidator{} + + // no tags registered for the slice itself yet, so only elements are validated + assert.NoError(t, v.ValidateStruct(invalidSlice)) + assert.Error(t, v.ValidateStruct(invalidVal)) + assert.NoError(t, v.ValidateStruct(validSlice)) + assert.NoError(t, v.ValidateStruct(&invalidSlice)) + assert.Error(t, v.ValidateStruct(&invalidVal)) + assert.NoError(t, v.ValidateStruct(&validSlice)) + + err := v.ValidateStruct(invalidSliceVal) + assert.Error(t, err) + assert.Len(t, err, 1) // only value error + + RegisterValidatorTag("gt=1", CustomSlice{}) + + assert.Error(t, v.ValidateStruct(invalidSlice)) + assert.Error(t, v.ValidateStruct(invalidVal)) + assert.NoError(t, v.ValidateStruct(validSlice)) + assert.Error(t, v.ValidateStruct(&invalidSlice)) + assert.Error(t, v.ValidateStruct(&invalidVal)) + assert.NoError(t, v.ValidateStruct(&validSlice)) + + err = v.ValidateStruct(invalidSliceVal) + assert.Error(t, err) + assert.Len(t, err, 2) // both slice length and value error +} + +func TestValidatorTagsMap(t *testing.T) { + type CustomMap map[string]struct { + B int `binding:"gt=0"` + } + + var ( + invalidMap = CustomMap{"12345678": {1}} + invalidKey = CustomMap{"123456789": {1}, "abcdefgh": {2}} + invalidVal = CustomMap{"12345678": {0}, "abcdefgh": {2}} + invalidMapVal = CustomMap{"12345678": {0}} + validMap = CustomMap{"12345678": {1}, "abcdefgh": {2}} + ) + + v := &defaultValidator{} + + // no tags registered for the map itself yet, so only values are validated + assert.NoError(t, v.ValidateStruct(invalidMap)) + assert.NoError(t, v.ValidateStruct(invalidKey)) + assert.Error(t, v.ValidateStruct(invalidVal)) + assert.NoError(t, v.ValidateStruct(validMap)) + assert.NoError(t, v.ValidateStruct(&invalidMap)) + assert.NoError(t, v.ValidateStruct(&invalidKey)) + assert.Error(t, v.ValidateStruct(&invalidVal)) + assert.NoError(t, v.ValidateStruct(&validMap)) + + err := v.ValidateStruct(invalidMapVal) + assert.Error(t, err) + assert.Len(t, err, 1) // only value error + + RegisterValidatorTag("gt=1,dive,keys,max=8,endkeys", CustomMap{}) + + assert.Error(t, v.ValidateStruct(invalidMap)) + assert.Error(t, v.ValidateStruct(invalidKey)) + assert.Error(t, v.ValidateStruct(invalidVal)) + assert.NoError(t, v.ValidateStruct(validMap)) + assert.Error(t, v.ValidateStruct(&invalidMap)) + assert.Error(t, v.ValidateStruct(&invalidKey)) + assert.Error(t, v.ValidateStruct(&invalidVal)) + assert.NoError(t, v.ValidateStruct(&validMap)) + + err = v.ValidateStruct(invalidMapVal) + assert.Error(t, err) + assert.Len(t, err, 2) // both map size and value errors +}