gin: Custom Binding Error Message

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”

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 3
  • Comments: 32 (4 by maintainers)

Most upvoted comments

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 😄

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!

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)

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

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
}

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

@sudo-suhas thanks, that should work for me

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

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

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

@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