Customizing Validation Error Messages in Gin
If you've worked with validations in Gin, you may have encountered technical and difficult-to-understand error messages. In this article, I'll show you how to customize these messages to make them more user-friendly and clear.
The Problem with Default Messages in Gin
Imagine a user trying to create an account but entering an incorrect email. Instead of a clear message like "The Email field must be a valid email," they receive something like:
{
"error": "Key: 'CreateUserInput.Email' Error:Field validation for 'Email' failed on the 'email' tag"
}
This can be confusing, especially for end users or non-technical testers.
Creating a Middleware to Customize Error Messages
Gin allows us to add middleware to modify the request or response before they reach the client. In this case, our middleware captures validation errors and formats them in a more understandable way.
Let's consider the following basic structure for our project:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type CreateUserInput struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
func main() {
r := gin.Default()
r.POST("/users", func(c *gin.Context) {
var input CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
})
r.Run(":8080")
}
Now, let's create middleware to handle validation errors:
To create middleware, it's simple. We can use the following basic structure:
func BasicMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Code to be executed before processing the request.
fmt.Println("Before")
// Call the next Handler in the middleware.
c.Next()
// Code to be executed after processing the request
fmt.Println("After")
}
}
Following this basic structure, we'll create middleware that executes our logic after processing the request.
Before processing the error, we'll define a structure for our response, a mapping of the messages we want to change based on the binding tags, and a handler to process the error.
// Reponse struct to be returned as JSON
type ErrorMsg struct {
Field string `json:"field"`
Message string `json:"message"`
}
// Map of error messages
var errorMessages = map[string]string{
"required": "The field %s is required",
"email": "The field %s must be a valid email",
}
// function to get the error message based on the field error
func getErrorMsg(fe validator.FieldError) string {
tag := fe.Tag()
field := fe.StructNamespace()
parts := strings.SplitN(field, ".", 2)
field = parts[len(parts)-1]
if msg, ok := errorMessages[tag]; ok {
if fe.Param() != "" {
return fmt.Sprintf(msg, field, fe.Param())
}
return fmt.Sprintf(msg, field)
}
return fmt.Sprintf("Validation error on field %s: %s", fe.Field(), tag)
}
func handleError(c *gin.Context, err error) {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
out := make([]ErrorMsg, len(ve))
for i, fe := range ve {
out[i] = ErrorMsg{fe.Field(), getErrorMsg(fe)}
}
c.JSON(http.StatusBadRequest, gin.H{"type": "ValidationError", "message": "Invalid Attributes", "errors": out})
return
}
}
With the above code, we have the basic functions that will be used in our middleware, so we can now create it.
func ErrorHandlerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
handleError(c, err)
}
}
}
Now that our middleware is created, we need to do two more steps:
- Add the middleware to the router for all routes.
- Pass the error to the context so that the error handler middleware can process and return the correct response.
The complete main.go file will look like this:
func main() {
r := gin.Default()
// Add the middleware to the route
r.Use(ErrorHandlerMiddleware())
r.POST("/users", func(c *gin.Context) {
var input CreateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
// The error is handled by the middleware
if err := c.Error(err); err != nil {
fmt.Print(err)
}
return
}
c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
})
r.Run(":8080")
}
With this, we now have the customized error messages mapped to each validation tag.
{
"errors": [
{
"field": "Email",
"message": "The field Email must be a valid email"
},
{
"field": "Password",
"message": "The field Password is required"
}
],
"message": "Invalid Attributes",
"type": "ValidationError"
}
In this article, we saw how to customize error messages in Gin to make them more user-friendly and easier to understand. This technique can improve the experience for users and testers. If you enjoyed the article, leave a comment or share your experience with validations in Gin!

Top comments (2)
Nice!
Very interesting way to customize gin validation errors.
(In javascript is easier haha!)
Thank you! Yes, I agree, JavaScript has its advantages in that aspect hahaha!