Custom validation errors
Tags: go, gin
Published: 2021-06-24 18:07:13

If you try to create new account using too short password, you will get error `Key: 'User.Password' Error:Field validation for 'Password' failed on the 'min' tag`. This is not really user friendly, so it should be changed for better user experience. Let's see how we can transform that to our own custom error messages. For that purpose we will create new Gin handler functions in `internal/server/middleware.go` file:

```go
func customErrors(ctx *gin.Context) {
  ctx.Next()
  if len(ctx.Errors) > 0 {
    for _, err := range ctx.Errors {
      // Check error type
      switch err.Type {
      case gin.ErrorTypePublic:
        // Show public errors only if nothing has been written yet
        if !ctx.Writer.Written() {
          ctx.AbortWithStatusJSON(ctx.Writer.Status(), gin.H{"error": err.Error()})
        }
      case gin.ErrorTypeBind:
        errMap := make(map[string]string)
        if errs, ok := err.Err.(validator.ValidationErrors); ok {
          for _, fieldErr := range []validator.FieldError(errs) {
            errMap[fieldErr.Field()] = customValidationError(fieldErr)
          }
        }

        status := http.StatusBadRequest
        // Preserve current status
        if ctx.Writer.Status() != http.StatusOK {
          status = ctx.Writer.Status()
        }
        ctx.AbortWithStatusJSON(status, gin.H{"error": errMap})
      default:
        // Log other errors
        log.Error().Err(err.Err).Msg("Other error")
      }
    }

    // If there was no public or bind error, display default 500 message
    if !ctx.Writer.Written() {
      ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": InternalServerError})
    }
  }
}

func customValidationError(err validator.FieldError) string {
  switch err.Tag() {
  case "required":
    return fmt.Sprintf("%s is required.", err.Field())
  case "min":
    return fmt.Sprintf("%s must be longer than or equal %s characters.", err.Field(), err.Param())
  case "max":
    return fmt.Sprintf("%s cannot be longer than %s characters.", err.Field(), err.Param())
  default:
    return err.Error()
  }
}
```

Constant `InternalServerError` is defined in `internal/server/server.go`:

```go
const InternalServerError = "Something went wrong!"
```

Let's use new Gin middleware in `internal/server/router.go`:

```go
func setRouter() *gin.Engine {
  // Creates default gin router with Logger and Recovery middleware already attached
  router := gin.Default()

  // Enables automatic redirection if the current route can't be matched but a
  // handler for the path with (without) the trailing slash exists.
  router.RedirectTrailingSlash = true

  // Create API route group
  api := router.Group("/api")
  api.Use(customErrors)
  {
    api.POST("/signup", gin.Bind(store.User{}), signUp)
    api.POST("/signin", gin.Bind(store.User{}), signIn)
  }

  authorized := api.Group("/")
  authorized.Use(authorization)
  {
    authorized.GET("/posts", indexPosts)
    authorized.POST("/posts", createPost)
    authorized.PUT("/posts", updatePost)
    authorized.DELETE("/posts/:id", deletePost)
  }

  router.NoRoute(func(ctx *gin.Context) { ctx.JSON(http.StatusNotFound, gin.H{}) })

  return router
}
```

As you can see, we are now using `customErrors` middleware in `api` group. But that's not the only change. Notice updated routes for signing in and signing up:

```go
api.POST("/signup", gin.Bind(store.User{}), signUp)
api.POST("/signin", gin.Bind(store.User{}), signIn)
```

With these changes, we are trying to bind request data before even hitting signUp and signIn handlers, which means that handlers will only be reached if form validations are passed. With setup like this, handlers don't need to think about binding errors, because there was none if handler is reached. With that in mind, let's update these 2 handlers:

```go
func signUp(ctx *gin.Context) {
  user := ctx.MustGet(gin.BindKey).(*store.User)
  if err := store.AddUser(user); err != nil {
    ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  ctx.JSON(http.StatusOK, gin.H{
    "msg": "Signed up successfully.",
    "jwt": generateJWT(user),
  })
}

func signIn(ctx *gin.Context) {
  user := ctx.MustGet(gin.BindKey).(*store.User)
  user, err := store.Authenticate(user.Username, user.Password)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Sign in failed."})
    return
  }

  ctx.JSON(http.StatusOK, gin.H{
    "msg": "Signed in successfully.",
    "jwt": generateJWT(user),
  })
}
```

Our handlers are much simpler now and they are only handling database errors. If you again try to create account with too short username and password, you will see more readable and descriptive errors:

![User friendly validation errors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7b4ntnxfjihey7ke8sjv.png)

We can now do same thing with Post handlers. I will not show you the solution here, so you can try it yourself for practice, but you can find it on [RGB GitHub repo](https://github.com/matijakrajnik/rgb).