Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Binding Error Message #430

Closed
dre1080 opened this issue Sep 11, 2015 · 32 comments
Closed

Custom Binding Error Message #430

dre1080 opened this issue Sep 11, 2015 · 32 comments

Comments

@dre1080
Copy link

dre1080 commented Sep 11, 2015

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"

@Leko
Copy link

Leko commented Sep 19, 2015

+1

@deankarn
Copy link

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 #393

@dre1080
Copy link
Author

dre1080 commented Sep 20, 2015

@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

@nazwa
Copy link

nazwa commented Sep 20, 2015

Let me add my 2 cents here 😄

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 😄

@paulm17
Copy link

paulm17 commented Feb 18, 2016

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

@deankarn
Copy link

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.

@paulm17
Copy link

paulm17 commented Feb 18, 2016

Thanks for getting back so quickly! Spinning my wheels and I figured it out. 👍

@deankarn
Copy link

deankarn commented Nov 16, 2016

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

@sudo-suhas
Copy link
Contributor

@joeybloggs This link is broken - https://github.com/go-playground/validator/tree/v9/examples/gin-upgrading-overriding

@deankarn
Copy link

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

@sudo-suhas
Copy link
Contributor

Working link for future reference - https://github.com/go-playground/validator/tree/v9/_examples/gin-upgrading-overriding

@Kaijun
Copy link

Kaijun commented Nov 6, 2017

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?

@sudo-suhas
Copy link
Contributor

sudo-suhas commented Nov 6, 2017

@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.

@Kaijun
Copy link

Kaijun commented Nov 6, 2017

@sudo-suhas thanks, that should work for me

@sudo-suhas
Copy link
Contributor

@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?

@deankarn
Copy link

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)

@gobeam
Copy link

gobeam commented Dec 11, 2018

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!

@kumarvimal
Copy link

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?

@surahmans
Copy link

surahmans commented Jan 21, 2020

@nazwa @roshanr83 Can we set the content-type to application/json instead of plain/text?

@gobeam
Copy link

gobeam commented Jan 21, 2020

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.

@gobeam
Copy link

gobeam commented Jan 21, 2020

@nazwa @roshanr83 Can we set the content-type to application/json instead of plain/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
}

@vaibhavpandeyvpz
Copy link

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().

@ivan-avalos
Copy link

@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.

@nazwa
Copy link

nazwa commented Feb 28, 2020

@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.

@mrfoh
Copy link

mrfoh commented Jun 6, 2020

@surahmans did @gobeam solution for getting application/json header work for you?

@maracko
Copy link

maracko commented Jul 16, 2021

What is the current way to create custom validation error messages? The one's listed above are not working

@shyandsy
Copy link

shyandsy commented Nov 16, 2021

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
}

@triadmoko
Copy link

Hi you can custom error message multiple language. may be solution for me
model.go

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
}

@ndasim
Copy link

ndasim commented Oct 22, 2022

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()})
			}
		}
	}
}

@ismailbayram
Copy link

@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.

@rodjenihm
Copy link

rodjenihm commented Feb 23, 2023

@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)

@ismailbayram
Copy link

@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)

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
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests