Anshul Goyal I love to code and use new technologies.

Gin binding in Go: A tutorial with examples

7 min read 2208

Go Logo

Over the past few years, Go has become very popular for microservices. Gin is a web framework for Go that focuses on performance and productivity and features Martini-like APIs.

In this tutorial, we’ll show you how to use Gin’s binding. We’ll walk you through request payload validation, writing custom validation using reflection and the validator module, and building custom bindings for various formats, such as TOML, etc.

What is Gin binding?

Gin binding is an awesome serialization library. It supports JSON, XML, query parameter, and more out of the box and comes with a built-in validation framework.

Gin bindings are used to serialize JSON, XML, path parameters, form data, etc. to structs and maps. It also has a baked-in validation framework with complex validations.

Gin supports various formats by providing struct tags. For example, the uri tag is used to serialize path parameters:

package main

import (
   "fmt"
   "github.com/gin-gonic/gin"
   "net/http"
)

type Body struct {
  // json tag to serialize json body
   Name string `json:"name"`
}

func main() {
   engine:=gin.New()
   engine.POST("/test", func(context *gin.Context) {
      body:=Body{}
      // using BindJson method to serialize body with struct
      if err:=context.BindJSON(&body);err!=nil{
         context.AbortWithError(http.StatusBadRequest,err)
         return
      }
      fmt.Println(body)
      context.JSON(http.StatusAccepted,&body)
   })
   engine.Run(":3000")
}

The above code snippet binds JSON payloads coming from the POST /test endpoint to the body struct instance. We can test it with the Postman tool, as shown below.

Gin Binding with Go Postman Test

BindJSON reads the body buffer to serialize it to a struct. BindJSON cannot be called on the same context twice because it flushes the body buffer.

If you want to serialize the body to two different structs, use ShouldBindBodyWith to copy the body buffer and add it to context.

if err:=context.ShouldBindBodyWith(&body,binding.JSON);err!=nil{
   context.AbortWithError(http.StatusBadRequest,err)
   return
}

In the same way, the XML body and path parameters are mapped to structs.

We made a custom demo for .
No really. Click here to check it out.

package main

import (
   "fmt"
   "github.com/gin-gonic/gin"
   "net/http"
)

// path paramter with name details will mapped to Details
type URI struct {
   Details string `json:"name" uri:"details"`
}

func main() {
   engine:=gin.New()
// adding path params to router
   engine.GET("/test/:details", func(context *gin.Context) {
      uri:=URI{}
      // binding to URI
      if err:=context.BindUri(&uri);err!=nil{
         context.AbortWithError(http.StatusBadRequest,err)
         return
      }
      fmt.Println(uri)
      context.JSON(http.StatusAccepted,&uri)
   })
   engine.Run(":3000")
}

The example above is a basic use case for binding to serialize the body, query, and path parameters. The above endpoint produces a serialized JSON object by binding path params, as shown below.

Serialized JSON Object

Basic validation using Gin

Gin uses the validator package internally for validations. This package validator provides an extensive set of inbuilt validations, including required, type validation, and string validation.

Validations are added to structs via the binding struct tag:

type URI struct {
   Details string `json:"name" uri:"details" binding:"required"`
}

The validator package also supports more complex validation, such as len ,max, and min.

Let’s use Gin’s inbuilt validation syntax for several practical scenarios.

Validating phone numbers, emails, and country codes

We often have to validate phone numbers, email addresses, and country codes in our web application backends while we are processing contact details. Look at the following example struct:

type Body struct {
   FirstName string `json:"firstName" binding:"required"`
   LastName string `json:"lastName" binding:"required"`
   Email string `json:"email" binding:"required,email"`
   Phone string `json:"phone" binding:"required,e164"`
   CountryCode string `json:"countryCode" binding:"required,iso3166_1_alpha2"`
}

The above struct tags validate email with a generic regular expression, phone with the international E.164 standard, and country-code with the ISO-3166–1 two-letter standard. For example, it accepts the following sample JSON payload for the binding process:

{
   "firstName": "John",
   "lastName": "Mark",
   "email": "[email protected]",
   "phone": "+11234567890",
   "countryCode": "US"
}

The validator package offers postal code validation support too. For example, you can validate a British postal code with the following syntax.

type Body struct {
    PostCode string `json:"postCode" binding:"required,postcode_iso3166_alpha2=GB"`
}

Validating custom strings formats

Earlier, we used some standard formats for validation. But, we often have to define custom formats for validating domain-specific user inputs. For example, you can use struct tags to validate a custom product code format. The validator package offers many helpful string validator helpers.

Assume that you need to validate a ten-letter product code starting with the PC string prefix. For this scenario, you can use the startswith tag with the len tag:

type Body struct {
   ProductCode string `json:"productCode" binding:"required,startswith=PC,len=10"`
}

The above struct definition accepts the following JSON payload for binding:

{
   "productCode": "PC00001120"
}

Here are some other string validation helpers that we often need:

Tag Description Usage Example
uppercase Accepts only uppercase letters binding:"uppercase"
lowercase Accepts only lowercase letters binding:"lowercase"
contains Accepts only strings that contain a specific string segment. binding:"contains=key"
alphanum Accepts only alphanumeric characters (English letters and numerical digits). Rejects strings that contain special characters. binding:"alphanum"
alpha Accepts only English letters binding:"alpha"
endswith Accepts only strings that end with a specific sequence of characters binding:"endswith=."

Comparing with fields and values

The validator package offers several tags for comparison — you can use these tags to compare a particular field with another field, or hardcoded value, as shown below:

type Body struct {
   Width int `json:"width" binding:"required,gte=1,lte=100,gtfield=Height"`
   Height int `json:"height" binding:"required,gte=1,lte=100"`
}

The above code binds JSON payloads into the above struct definition based on the following constraints:

  • Width: 1 ≤ x ≤100 and greater than the Height value
  • Height: 1 ≤ x ≤100

Validating date and time

Gin offers the time_format struct tag to validate date and time formats. You can combine the time_format tag with validation helper tags for date and time validation.

For example, you can validate a date range form input with the following struct definition:

type Body struct {
   StartDate time.Time `form:"start_date" binding:"required,ltefield=EndDate" time_format:"2006-01-02"`
   EndDate time.Time `form:"end_date" binding:"required" time_format:"2006-01-02"`
}

You can provide any valid Go date format via the time_format tag. For example, 2006-01-02 15:04:05 will accept a date-time input based on the yyyy-mm-dd hh:mm:ss format.

Nested structs validation

Nested structs and arrays are also validated recursively.

type User struct {
   Name string `json:"name" binding:"required,min=3"`
   Age uint `json:"age" binding:"required,min=18"`
   Comments []*Comment `json:"comments" binding:"required"`
}

type Comment struct {
   Text string `json:"text" binding:"required,max=255"`
   Type string `json:"type" binding:"required,oneof=post nested"`
}

Gin comes with many inbuilt validations and validation helper tags; you can find an exhaustive list on GitHub.

Handling validation errors

In previous examples, we used the AbortWithError function to send an HTTP error code back to the client, but we didn’t send a meaningful error message. So, we can improve endpoints by sending a meaningful validation error message as a JSON output:

package main
import (
  "github.com/gin-gonic/gin"
  "net/http"
)
type Body struct {
   Price uint `json:"price" binding:"required,gte=10,lte=1000"`
}
func main() {
  engine:=gin.New()
  engine.POST("/test", func(context *gin.Context) {
     body:=Body{}
     if err:=context.ShouldBindJSON(&body);err!=nil{
        context.AbortWithStatusJSON(http.StatusBadRequest,
        gin.H{
            "error": "VALIDATEERR-1",
            "message": "Invalid inputs. Please check your inputs"})
        return
     }
     context.JSON(http.StatusAccepted,&body)
  })
  engine.Run(":3000")
}

Now, the above code uses the AbortWithStatusJSON function and returns a unique error code and message to the client, so the client can display a meaningful error message to the user.
AbortwithStatusJSON
Also, you can return the technical auto-generated error message, as shown below:

gin.H{
    "error": "VALIDATEERR-1",
    "message": err.Error()})

The above approaches give a too-generic error and a technical message respectively, so we can improve error responses further by returning a list of meaningful error messages with the following code:

package main
import (
  "github.com/gin-gonic/gin"
  "github.com/go-playground/validator/v10"
  "net/http"
  "errors"
)

type Body struct {
   Product string `json:"product" binding:"required,alpha"`
   Price uint `json:"price" binding:"required,gte=10,lte=1000"`
}

type ErrorMsg struct {
    Field string `json:"field"`
    Message   string `json:"message"`
}

func getErrorMsg(fe validator.FieldError) string {
    switch fe.Tag() {
        case "required":
            return "This field is required"
        case "lte":
            return "Should be less than " + fe.Param()
        case "gte":
            return "Should be greater than " + fe.Param()
    }
    return "Unknown error"
}

func main() {
  engine:=gin.New()
  engine.POST("/test", func(context *gin.Context) {
     body:=Body{}
     if err:=context.ShouldBindJSON(&body);err!=nil{
        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)}
            }
            context.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errors": out})
        }
        return
     }
     context.JSON(http.StatusAccepted,&body)
  })
  engine.Run(":3000")
}

Now, we have clear and meaningful error messages based on validation tag names. For example, if you send the following JSON payload to the API:

{
    "price": 5
}

You will get the following output:

{
    "errors": [
        {
            "field": "Product",
            "message": "This field is required"
        },
        {
            "field": "Price",
            "message": "Should be greater than 10"
        }
    ]
}

Now you can display a detailed and specific error message for each field by using the above errors list.

Writing custom validations

Not all use cases are well-suited to built-in Gin validations. For this reason, Gin provides methods to add custom validations.

The reflect package is used during the validation process to figure out types and the value of struct fields at runtime.

To create a new binding, you have to register a validation with a function that performs the validation.

 // getting the validation engine and type casting it.
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
   // registering validation for nontoneof
   v.RegisterValidation("notoneof", func(fl validator.FieldLevel) bool {
     // split values using ` `. eg. notoneof=bob rob job
      match:=strings.Split(fl.Param()," ")
     // convert field value to string
      value:=fl.Field().String()
      for _,s:=range match {
       // match value with struct filed tag
         if s==value {
            return false
         }
      }
      return true
   })
}

You can access the validation engine using the binding package for adding custom validators. The Validator variable is exported. Validator provides the Engine method, which returns the validation engine.

The RegisterValidation method on the engine takes a name and function that returns whether the field is valid or not.

You can access parameters passed to the validator using the Param method.

The Field method returns the value of the field in a struct. The value can be typecast to various data types.

validator.FieldLevel has access to a whole struct. You can also access different keys of a parent struct.

Accessing other struct fields

FieldLevel has a Top method that returns a reflect.Value type of the struct. That can be used to access the field in a struct.

For example, you can create a validation where two fields can’t have the same value using reflect.Value.

v.RegisterValidation("unique", func(fl validator.FieldLevel) bool {
  // get the fields which need to be unique
   match:=strings.Split(fl.Param()," ")
  // value of the field
   value:=fl.Field().String()
   for _,s:=range match {
     // access to struct and getting value by field name
      fs:=fl.Top().FieldByName(s)
      // check only for string validation
      if fs.Kind() == reflect.String {
          // check value of both fields
         if value==fs.String() {
            return false
         }
      }
   }
   return true
})

The above example only checks for string values, but you can easily modify it for all data types:

type ExampleStruct struct {
   Name string `json:"name" uri:"name" binding:"notoneof=bob rob job"`
   LastName string `json:"last_name" binding:"unique=Name"`
}

Writing custom Gin bindings

In some cases, the client and server use different formats to interchange data. For example, instead of JSON or XML, TOML might be used as the body for a request.

For cases like this, Gin provides a plug-and-play method for changing the body parser.

Every binding needs to implement this interface. The Name method returns a binding name and the Bind methods parse the request body:

type Binding interface {
   Name() string
   Bind(*http.Request, interface{}) error
}

Here’s an example of binding:

type Toml struct {
}

// return the name of binding
func (t Toml) Name() string {
   return "toml"
}

// parse request
func (t Toml) Bind(request *http.Request, i interface{}) error {
// using go-toml package 
   tD:= toml.NewDecoder(request.Body)
// decoding the interface
   return tD.Decode(i)
}

Usage example:

engine.POST("/Toml", func(context *gin.Context) {
   uri:= URI{}

   if err:=context.MustBindWith(&uri, Toml{});err!=nil{
      context.AbortWithError(http.StatusBadRequest,err)
      return
   }
   context.JSON(200,uri)
})

Implementing BindBody to use ShouldBindBodyWith:

func (t Toml) BindBody(bytes []byte, i interface{}) error {
   return toml.Unmarshal(bytes,i)
}

Usage example:

engine.POST("/Toml", func(context *gin.Context) {
   uri:= URI{}

   if err:=context.ShouldBindBodyWith(&uri, Toml{});err!=nil{
      context.AbortWithError(http.StatusBadRequest,err)
      return
   }
   context.JSON(200,uri)
})

Conclusion

In this tutorial, we covered Gin binding, various built-in validators, and some more advanced use cases. We also covered how to build a custom binding using various interfaces provided by the Gin library. Finally, we build some custom validators using advanced reflection and the validator package. You can use these building blocks to build various HTTP body parsers.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Anshul Goyal I love to code and use new technologies.

Leave a Reply