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"
Comment From: Leko
+1
Comment From: deankarn
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
Comment From: dre1080
@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
Comment From: nazwa
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: 1. Log all private errors and display generic error to client (for things that went wrong) 2. Display public errors to client 3. Parse Bind errors and display to client
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:
Comment From: paulm17
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
Comment From: deankarn
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.
Comment From: paulm17
Thanks for getting back so quickly! Spinning my wheels and I figured it out. :+1:
Comment From: deankarn
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
Comment From: sudo-suhas
@joeybloggs This link is broken - https://github.com/go-playground/validator/tree/v9/examples/gin-upgrading-overriding
Comment From: deankarn
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
Comment From: sudo-suhas
Working link for future reference - https://github.com/go-playground/validator/tree/v9/_examples/gin-upgrading-overriding
Comment From: Kaijun
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?
Comment From: sudo-suhas
@Kaijun Have you tried vendoring your dependencies? You could use gin@1.2.0 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.
Comment From: Kaijun
@sudo-suhas thanks, that should work for me
Comment From: sudo-suhas
@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?
Comment From: deankarn
just my 2 cents, but it can be solved one of two ways:
1. Expose the binding.Validator to allow it to be overridden as before
2. Update to v9 with breaking changes
Too 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)
Comment From: gobeam
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!
Comment From: kumarvimal
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?
Comment From: surahmans
@nazwa @roshanr83 Can we set the content-type to application/json instead of plain/text?
Comment From: gobeam
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.
Comment From: gobeam
@nazwa @roshanr83 Can we set the content-type to
application/jsoninstead 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
}
Comment From: vaibhavpandeyvpz
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().
Comment From: ivan-avalos
@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.
Comment From: nazwa
@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.
Comment From: mrfoh
@surahmans did @gobeam solution for getting application/json header work for you?
Comment From: maracko
What is the current way to create custom validation error messages? The one's listed above are not working
Comment From: shyandsy
Hi
I suggest to try the third part package ShyGinErrors
first, you can define the validate rule with customize error message key in the data model.
// error message key value
var requestErrorMessage = map[string]string{
"error_invalid_email": "please input a valid email",
"error_invalid_username": "username must be alphanumric, with length 6-32",
"error_invalid_password": "password length 6-32",
}
// specific
type RegisterForm struct {
Email string `json:"email" binding:"required,email" msg:"error_invalid_email"`
Username string `json:"username" binding:"required,alphanum,gte=6,lte=32" msg:"error_invalid_username"`
Password string `json:"password" binding:"required,gte=6,lte=32" msg:"error_invalid_password"`
}
then, we can initialize the ShyGinError and use it to parse the err return by gin.BindJson()
ge = NewShyGinErrors(requestErrorMessage)
req := model.RegisterForm{}
if err := reqCtx.Gin().BindJSON(&req); err != nil {
// get key value error messages: { "username":"username must be alphanumric, with length 6-32"}
errors := ge.ListAllErrors(req, err)
// error handling
}
**Comment From: triadmoko**
Hi you can custom error message multiple language. may be solution for me
model.go
```golang
type LoginUser struct {
Email string `json:"email"`
Password string `json:"password"`
}
handler.go
lang := c.GetHeader("Accept-Language")
var req models.LoginUser
err := c.ShouldBindJSON(&req)
validation := req.Validation(lang)
if len(validation) > 0 {
errorMessage := gin.H{"errors": validation}
respon := helpers.ResponseApi("Failed Register", http.StatusBadRequest, "Failed", errorMessage)
c.JSON(http.StatusBadRequest, respon)
return
}
validation.go
func (v LoginUser) Validation(lang string) []string {
message := []string{}
if lang == "id" {
if len(v.Email) < 1 {
message = append(message, "invalid email :")
}
if len(v.Email) < 1 {
message = append(message, "invalid email :")
}
} else if lang == "en" {
if len(v.Email) < 1 {
message = append(message, "invalid email :")
}
if len(v.Email) < 1 {
message = append(message, "invalid email :")
}
}
return message
}
Comment From: ndasim
Okey, let me drop another stone into the well. I upgraded the code of @gobeam,@nazwa for the v10 validator.
package middleware
import (
"errors"
"fmt"
"net/http"
"strings"
"unicode"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/stvp/rollbar"
)
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()})
}
}
}
}
Comment From: ismailbayram
@ndasim Hi, thank you for your solution. But I could not solve the problem which is that response content type is text instead of json.
Comment From: rodjenihm
@ismailbayram I assume it is because you are using c.Bind or c.BindJSON instead of c.ShouldBindJSON.
c.Bind sets content type to text/plain under the hood with next line of code:
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
Comment From: ismailbayram
@ismailbayram I assume it is because you are using
c.Bindorc.BindJSONinstead ofc.ShouldBindJSON.c.Bindsets content type totext/plainunder the hood with next line of code:c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
Thank you for your reply, but I've figured out myself like below
if err := ctx.ShouldBindJSON(&yourStruct); err != nil {
ctx.Error(err).SetType(gin.ErrorTypeBind)
return
}