Search code examples
gogo-gin

More elegant way of validate body in go-gin


Is there a more elegant way to validate json body and route id using go-gin?

package controllers

import (
    "giin/inputs"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

func GetAccount(context *gin.Context) {

    // validate if `accountId` is valid `uuid``
    _, err := uuid.Parse(context.Param("accountId"))
    if err != nil {
        context.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
        return
    }

    // some logic here...

    context.JSON(http.StatusOK, gin.H{"message": "account received"})
}

func AddAccount(context *gin.Context) {

    // validate if `body` is valid `inputs.Account`
    var input inputs.Account
    if error := context.ShouldBindJSON(&input); error != nil {
        context.JSON(http.StatusBadRequest, error.Error())
        return
    }

    // some logic here...

    context.JSON(http.StatusOK, gin.H{"message": "account added"})
}

I created middleware which is able to detect if accountId was passed and if yes validate it and return bad request if accountId was not in uuid format but I couldn't do the same with the body because AccountBodyMiddleware tries to validate every request, could someone help me with this?

And also it would be nice if I could validate any type of body instead creating new middleware for each json body

package main

import (
    "giin/controllers"
    "giin/inputs"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

func AccountIdMiddleware(c *gin.Context) {
    id := c.Param("accountId")
    if id == "" {
        c.Next()
        return
    }
    if _, err := uuid.Parse(id); err != nil {
        c.JSON(http.StatusBadRequest, "uuid not valid")
        c.Abort()
        return
    }
}

func AccountBodyMiddleware(c *gin.Context) {
    var input inputs.Account
    if error := c.ShouldBindJSON(&input); error != nil {
        c.JSON(http.StatusBadRequest, "body is not valid")
        c.Abort()
        return
    }
    c.Next()
}

func main() {
    r := gin.Default()
    r.Use(AccountIdMiddleware)
    r.Use(AccountBodyMiddleware)

    r.GET("/account/:accountId", controllers.GetAccount)
    r.POST("/account", controllers.AddAccount)
    r.Run(":5000")
}

Solution

  • Using middlewares is certainly not the way to go here, your hunch is correct! Using FastAPI as inspiration, I usually create models for every request/response that I have. You can then bind these models as query, path, or body models. An example of query model binding (just to show you that you can use this to more than just json post requests):

    type User struct {
        UserId            string                     `form:"user_id"`
        Name              string                     `form:"name"`
    }
    
    func (user *User) Validate() errors.RestError {
        if _, err := uuid.Parse(id); err != nil {
            return errors.BadRequestError("user_id not a valid uuid")
        }
        return nil
    }
    

    Where errors is just a package you can define locally, so that can return validation errors directly in the following way:

    func GetUser(c *gin.Context) {
    
        // Bind query model
        var q User
        if err := c.ShouldBindQuery(&q); err != nil {
            restError := errors.BadRequestError(err.Error())
            c.JSON(restError.Status, restError)
            return
        }
    
        // Validate request
        if err := q.Validate(); err != nil {
            c.JSON(err.Status, err)
            return
        }
    
        // Business logic goes here
    }
    
    

    Bonus: In this way, you can also compose structs and call internal validation functions from a high level. I think this is what you were trying to accomplish by using middlewares (composing validation):

    type UserId struct {
        Id string
    }
    
    func (userid *UserId) Validate() errors.RestError {
        if _, err := uuid.Parse(id); err != nil {
            return errors.BadRequestError("user_id not a valid uuid")
        }
        return nil
    }
    
    type User struct {
        UserId
        Name string 
    }
    
    func (user *User) Validate() errors.RestError {
        if err := user.UserId.Validate(); err != nil {
            return err
        }
    
        // Do some other validation
        
        return nil
    }
    

    Extra bonus: read more about backend route design and model-based validation here if you're interested Softgrade - In Depth Guide to Backend Route Design

    For reference, here is an example errors struct:

    type RestError struct {
        Message string `json:"message"`
        Status  int    `json:"status"`
        Error   string `json:"error"`
    }
    
    func BadRequestError(message string) *RestError {
        return &RestError{
            Message: message,
            Status:  http.StatusBadRequest,
            Error:   "Invalid Request",
        }
    }