After calling c.Bind(&form)
, is it possible to provide a custom validation error message per field rather than the generic "Field validation for '...' failed on the '...' tag"
+1
Assuming your using the default validator, v5, used by default in gin.
The return error value for Bind is actually the type https://godoc.org/gopkg.in/bluesuncorp/validator.v5#StructErrors so you should be able to typecast the error to that, run the Flatten() to better represent the field errors and then you'll end up with a map[string]*FieldError which you can range over and create your own error message. See FieldError https://godoc.org/gopkg.in/bluesuncorp/validator.v5#FieldError
err := c.Bind(&form)
// note should check the error type before assertion
errs := err.(*StructErrors)
for _, fldErr := range errs.Flatten() {
// make your own error messages using fldErr which is a *FieldError
}
*Note the validator is currently at v8 which works slightly different but will still return the map of errors, hopefully it will be updated soon as v8 is much simpler and powerful, see https://github.com/gin-gonic/gin/issues/393
@joeybloggs That is what I'm currently doing.. would like a way to set custom error messages.. doing it that way just seems redundant and would rather just not use c.Bind
as my code would be much neater and cooler without it.. at this point I really don't see the point of c.Bind
Let me add my 2 cents here :smile:
I have an error handling middleware that handles all the parsing for me. Gin allows you to set different types of errors, which makes error handling a breeze. But you need to parse the bind errors 'manually' to get nice responses out. All of it can be wrapped in 3 stages:
You can see the all three in action below, but what's most important here is the case gin.ErrorTypeBind:
and ValidationErrorToText()
. The below could definitely be optimized, but so far it works great for my apps!
package middleware
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/kardianos/service"
"github.com/stvp/rollbar"
"gopkg.in/bluesuncorp/validator.v5"
"net/http"
)
var (
ErrorInternalError = errors.New("Woops! Something went wrong :(")
)
func ValidationErrorToText(e *validator.FieldError) string {
switch e.Tag {
case "required":
return fmt.Sprintf("%s is required", e.Field)
case "max":
return fmt.Sprintf("%s cannot be longer than %s", e.Field, e.Param)
case "min":
return fmt.Sprintf("%s must be longer than %s", e.Field, e.Param)
case "email":
return fmt.Sprintf("Invalid email format")
case "len":
return fmt.Sprintf("%s must be %s characters long", e.Field, e.Param)
}
return fmt.Sprintf("%s is not valid", e.Field)
}
// This method collects all errors and submits them to Rollbar
func Errors(env, token string, logger service.Logger) gin.HandlerFunc {
rollbar.Environment = env
rollbar.Token = token
return func(c *gin.Context) {
c.Next()
// Only run if there are some errors to handle
if len(c.Errors) > 0 {
for _, e := range c.Errors {
// Find out what type of error it is
switch e.Type {
case gin.ErrorTypePublic:
// Only output public errors if nothing has been written yet
if !c.Writer.Written() {
c.JSON(c.Writer.Status(), gin.H{"Error": e.Error()})
}
case gin.ErrorTypeBind:
errs := e.Err.(*validator.StructErrors)
list := make(map[string]string)
for field, err := range errs.Errors {
list[field] = ValidationErrorToText(err)
}
// Make sure we maintain the preset response status
status := http.StatusBadRequest
if c.Writer.Status() != http.StatusOK {
status = c.Writer.Status()
}
c.JSON(status, gin.H{"Errors": list})
default:
// Log all other errors
rollbar.RequestError(rollbar.ERR, c.Request, e.Err)
if logger != nil {
logger.Error(e.Err)
}
}
}
// If there was no public or bind error, display default 500 message
if !c.Writer.Written() {
c.JSON(http.StatusInternalServerError, gin.H{"Error": ErrorInternalError.Error()})
}
}
}
}
If you use something like this together with the binding middleware, your handlers never need to think about errors. Handler is only executed if the form passed all validations and in case of any errors the above middleware takes care of everything!
r.POST("/login", gin.Bind(LoginStruct{}), LoginHandler)
(...)
func LoginHandler(c *gin.Context) {
var player *PlayerStruct
login := c.MustGet(gin.BindKey).(*LoginStruct)
}
Hope it helps a little :smile:
I can't get the Errors map:
error is validator.ValidationErrors, not *validator.StructErrors
Also is it:
"gopkg.in/bluesuncorp/validator.v5"
or
"gopkg.in/go-playground/validator.v8"
Tried for an hour to get something other than.
Key: 'Form.Password' Error:Field validation for 'Password' failed on the 'required' tag
I'd like to stick with c.Bind and stay within GIN. Other than use the validation lib directly.
Thanks
It's definitely http://gopkg.in/go-playground/validator.v8
It was very recently updated from v5 to v8 perhaps you just need to ensure the libs are updated?
And the return value in v5 used to be StructError but now is ValidationErrors which is a flattened and much easier to parse map of errors.
Thanks for getting back so quickly! Spinning my wheels and I figured it out. :+1:
just for everyones information as of validator v9.1.0 custom validation errors are possible and are i18n and l10n aware using universal-translator and locales
click here for instructions to upgrade gin to validator v9
@joeybloggs This link is broken - https://github.com/go-playground/validator/tree/v9/examples/gin-upgrading-overriding
Oh I changed the examples folder to _examples a while ago to avoid pulling in any external example dependencies, if any, when using go get, just modify the URL and the example is still there
Working link for future reference - https://github.com/go-playground/validator/tree/v9/_examples/gin-upgrading-overriding
since @sudo-suhas has added a new function RegisterValidation
to binding.Validator
which accepts v8.Validator.Func, now i can not override the defaultValidator anymore.
@joeybloggs do you have any solutions?
@Kaijun Have you tried vendoring your dependencies? You could use [email protected]
by following these instructions - https://github.com/gin-gonic/gin#use-a-vendor-tool-like-govendor.
One possible way to resolve this would be to export the validator instance itself. That way I can call RegisterValidaton
directly without modifying the StructValidator
interface.
@sudo-suhas thanks, that should work for me
@javierprovecho What do you suggest? Shall I make a PR to remove RegisterValidation
from the interface so that we are not locked into validator@v8
? Or perhaps move forward with #1015?
just my 2 cents, but it can be solved one of two ways:
binding.Validator
to allow it to be overridden as beforeToo keep Gin configurable I would expose binding.Validator
no matter the decision. I also cannot recommend updating to v9 enough, breaking or not(but I am a little bias)
I know this is old but I took liberty and try to little modify the code of @nazwa in accordance with "gopkg.in/go-playground/validator.v8" and also to get errors a little bit more readable
package middleware
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/stvp/rollbar"
"gopkg.in/go-playground/validator.v8"
"net/http"
"strings"
"unicode"
"unicode/utf8"
)
var (
ErrorInternalError = errors.New("whoops something went wrong")
)
func UcFirst(str string) string {
for i, v := range str {
return string(unicode.ToUpper(v)) + str[i+1:]
}
return ""
}
func LcFirst(str string) string {
return strings.ToLower(str)
}
func Split(src string) string {
// don't split invalid utf8
if !utf8.ValidString(src) {
return src
}
var entries []string
var runes [][]rune
lastClass := 0
class := 0
// split into fields based on class of unicode character
for _, r := range src {
switch true {
case unicode.IsLower(r):
class = 1
case unicode.IsUpper(r):
class = 2
case unicode.IsDigit(r):
class = 3
default:
class = 4
}
if class == lastClass {
runes[len(runes)-1] = append(runes[len(runes)-1], r)
} else {
runes = append(runes, []rune{r})
}
lastClass = class
}
for i := 0; i < len(runes)-1; i++ {
if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) {
runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...)
runes[i] = runes[i][:len(runes[i])-1]
}
}
// construct []string from results
for _, s := range runes {
if len(s) > 0 {
entries = append(entries, string(s))
}
}
for index, word := range entries {
if index == 0 {
entries[index] = UcFirst(word)
} else {
entries[index] = LcFirst(word)
}
}
justString := strings.Join(entries," ")
return justString
}
func ValidationErrorToText(e *validator.FieldError) string {
word := Split(e.Field)
switch e.Tag {
case "required":
return fmt.Sprintf("%s is required", word)
case "max":
return fmt.Sprintf("%s cannot be longer than %s", word, e.Param)
case "min":
return fmt.Sprintf("%s must be longer than %s", word, e.Param)
case "email":
return fmt.Sprintf("Invalid email format")
case "len":
return fmt.Sprintf("%s must be %s characters long", word, e.Param)
}
return fmt.Sprintf("%s is not valid", word)
}
// This method collects all errors and submits them to Rollbar
func Errors() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// Only run if there are some errors to handle
if len(c.Errors) > 0 {
for _, e := range c.Errors {
// Find out what type of error it is
switch e.Type {
case gin.ErrorTypePublic:
// Only output public errors if nothing has been written yet
if !c.Writer.Written() {
c.JSON(c.Writer.Status(), gin.H{"Error": e.Error()})
}
case gin.ErrorTypeBind:
errs := e.Err.(validator.ValidationErrors)
list := make(map[string]string)
for _,err := range errs {
list[err.Field] = ValidationErrorToText(err)
}
// Make sure we maintain the preset response status
status := http.StatusBadRequest
if c.Writer.Status() != http.StatusOK {
status = c.Writer.Status()
}
c.JSON(status, gin.H{"Errors": list})
default:
// Log all other errors
rollbar.RequestError(rollbar.ERR, c.Request, e.Err)
}
}
// If there was no public or bind error, display default 500 message
if !c.Writer.Written() {
c.JSON(http.StatusInternalServerError, gin.H{"Error": ErrorInternalError.Error()})
}
}
}
}
P.S @nazwa thanx for your solution really appreciate it!
middleware, written by @nazwa and modified by @roshanr83 is working perfectly fine, Only things I am missing here is the field's JSON tag. any way to get json tag in error messages?
@nazwa @roshanr83 Can we set the content-type to application/json
instead of plain/text
?
middleware, written by @nazwa and modified by @roshanr83 is working perfectly fine, Only things I am missing here is the field's JSON tag. any way to get json tag in error messages?
I'vent tried it yet but I think you can access field's JSON tag in one of field of struct validator.FieldError.
@nazwa @roshanr83 Can we set the content-type to
application/json
instead ofplain/text
?
Try this on your controller method:
if err := c.ShouldBindBodyWith(&yourBindingStruct, binding.JSON); err != nil {
_ = c.AbortWithError(http.StatusUnprocessableEntity, err).SetType(gin.ErrorTypeBind)
return
}
middleware, written by @nazwa and modified by @roshanr83 is working perfectly fine, Only things I am missing here is the field's JSON tag. any way to get json tag in error messages?
e.Field()
is already the JSON field name, the struct field name is accessible at StructField()
.
@gobeam So, now, how can I implement the handler? I'm currently unable to catch errors, I'm trying with r.Use(utils.Errors())
and len(c.Errors)
is always 0.
@gobeam @ivan-avalos is this what you're looking for?
type uploadPhotoParams struct {
ContentSize int64 `json:"contentSize"`
}
r.POST("/upload-photo/:albumId", gin.Bind(uploadPhotoParams{}), uploadPhoto)
func uploadPhoto(c *gin.Context) {
postForm := c.MustGet(gin.BindKey).(*uploadPhotoParams)
(...)
}
That way gin handles your binding automatically, and all errors are processed before your handler is even hit. This way you have a guarantee of a valid params object inside your handler.
@surahmans did @gobeam solution for getting application/json
header work for you?
Most helpful comment
Let me add my 2 cents here :smile:
I have an error handling middleware that handles all the parsing for me. Gin allows you to set different types of errors, which makes error handling a breeze. But you need to parse the bind errors 'manually' to get nice responses out. All of it can be wrapped in 3 stages:
You can see the all three in action below, but what's most important here is the
case gin.ErrorTypeBind:
andValidationErrorToText()
. The below could definitely be optimized, but so far it works great for my apps!If you use something like this together with the binding middleware, your handlers never need to think about errors. Handler is only executed if the form passed all validations and in case of any errors the above middleware takes care of everything!
Hope it helps a little :smile: