Learn Gin - Request Binding & Validation
Series/Learn Gin/Episode 5
Episode 5 of 23

Learn Gin - Request Binding & Validation

This episode dissects Gin's request binding: JSON, query, form, URI, XML, YAML, and TOML; understanding the struct tags json, form, uri, and binding; the go-playground/validator/v10 library; and the differences between ShouldBind, MustBind, and Bind.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

In episode 4 you already learned to design API URLs. Now we get to the part that's often underestimated but causes the most bugs: mapping request data to structs (binding) and making sure the data is valid (validation). If these two aren't handled correctly, your API will accept dirty data.

Episode 5 dissects binding thoroughly: the various formats Gin supports, the struct tags that control mapping, the validator library that enforces rules, and when to use ShouldBind, MustBind, or Bind.

Binding Various Formats

JSON, Query, and Form

Gin maps request data to a struct through the appropriate tags. JSON data is read from the body, query data from the URL, and form data from an application/x-www-form-urlencoded or multipart body.

Struct with json, form, uri tags
type SearchRequest struct {
    Query string `json:"query" form:"q" uri:"q" binding:"required"`
    Page  int    `json:"page" form:"page" uri:"page" binding:"gte=1"`
    Limit int    `json:"limit" form:"limit" uri:"limit" binding:"min=1,max=100"`
}
 
type CreateUser struct {
    Name  string `json:"name" form:"name" binding:"required,min=3"`
    Email string `json:"email" form:"email" binding:"required,email"`
    Age   int    `json:"age" form:"age" binding:"gte=17,lte=100"`
}

The form:"q" tag lets a field be filled via query or form. The uri:"q" tag enables filling via a path parameter. Structures like this are the foundation of a clean REST API.

Binding JSON from the Body

To read a JSON body, call ShouldBindJSON:

Binding a JSON body
func createUserHandler(c *gin.Context) {
    var user CreateUser
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    c.JSON(201, gin.H{"name": user.Name, "email": user.Email})
}

The function c.ShouldBindJSON(&user) reads c.Request.Body, decodes the JSON into the struct, then runs validation. If the Content-Type doesn't match, use the approach we'll discuss shortly.

Binding Query, URI, and Form

Gin provides specific methods for each data source:

Bind query, uri, and form
// query: /search?q=golang&page=2&limit=20
var q SearchRequest
c.ShouldBindQuery(&q)
 
// uri: /search/golang/2/20
c.ShouldBindUri(&q)
 
// form: body application/x-www-form-urlencoded
var form CreateUser
c.ShouldBind(&form)

c.ShouldBind(&form) picks a binder automatically based on the request's Content-Type: JSON, XML, or form. Meanwhile, ShouldBindQuery, ShouldBindUri, and ShouldBindJSON force a specific format.

Validation with go-playground/validator

Binding Tags as Validator Rules

Gin uses the go-playground/validator/v10 library behind the scenes. The binding tag defines validation rules that are applied right after a successful bind:

  • required — field is mandatory.
  • min=3, max=20 — minimum and maximum lengths.
  • email — must be a valid email address.
  • gte=1,lte=100 — value must be within a range.
  • oneof=admin user — value must be one of the listed options.
Validation with various rules
type RegisterInput struct {
    Username string `json:"username" binding:"required,min=4,max=16,alphanum"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=8"`
    Role     string `json:"role" binding:"oneof=user admin"`
    Score    int    `json:"score" binding:"gte=0,lte=100"`
}

The oneof=user admin tag ensures the Role field only contains user or admin. Combinations of min, max, email, and alphanum keep input clean from the start.

For complex data, the validator also descends into nested structs with the dive tag. The dive tag instructs the validator to validate every element of a slice; without dive, validation only checks the slice itself, not its contents.

ShouldBind vs MustBind vs Bind

Understanding the Three Approaches

  • ShouldBind*: returns an error without changing the response status; you handle the error yourself.
  • MustBind*: on error, immediately returns 400 with an error JSON message; the code after it isn't executed.
  • Bind: the old version (deprecated since Gin v1.14 for many cases), automatically picks a binder based on Content-Type and writes 400 on error.
Comparing ShouldBind and MustBind
// pola yang disarankan
var user CreateUser
if err := c.ShouldBindJSON(&user); err != nil {
    c.JSON(400, gin.H{"error": "data tidak valid"})
    return
}
 
// pola cepat, tidak fleksibel
c.MustBindWith(&user, binding.JSON)
c.JSON(200, gin.H{"user": user})

c.MustBindWith(&user, binding.JSON) writes a 400 response automatically when binding fails. For full control over error messages, ShouldBindJSON remains the best choice.

The validator returns errors of type validator.ValidationErrors. With errors.As(err, &verrs) you can check whether the error comes from the validator, then read the details of each FieldError such as field, tag, and the failing value — useful for building consistent error responses across your whole API.

Advanced Binding Formats

XML, YAML, and TOML

Besides JSON, Gin supports binding XML, YAML, and TOML:

Binding XML and YAML
var xmlData MyXML
c.ShouldBindXML(&xmlData)
 
var yamlData MyYAML
c.ShouldBindBodyWithYAML(&yamlData)
 
var tomlData MyTOML
c.ShouldBindBodyWithTOML(&tomlData)

The methods c.ShouldBindBodyWithYAML(&yamlData) and c.ShouldBindBodyWithTOML have been available since Gin v1.10. The ShouldBindBodyWith* family can also be called multiple times within a single handler because it stores a copy of the body in the Context — unlike ShouldBind, which can only read the body once.

Custom Validators with RegisterValidation

If the built-in rules aren't enough, register a custom validator:

Register a custom validator
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
    v.RegisterValidation("matauang", func(fl validator.FieldLevel) bool {
        return fl.Field().String() == "IDR" || fl.Field().String() == "USD"
    })
}
 
type Payment struct {
    Currency string `json:"currency" binding:"required,matauang"`
}

The function binding.Validator.Engine() returns the validator instance Gin uses, so the matauang rule is immediately available to every binding:"matauang" tag.

Closing

Key takeaways:

  • Binding maps requests to structs: JSON, query, form, URI, XML, YAML, TOML.
  • Struct tags determine source and rules: json, form, uri, binding.
  • The validator uses go-playground/validator/v10: required, email, min, max, oneof.
  • ShouldBind returns an error, MustBind writes 400 automatically.
  • Custom rules via RegisterValidation.
  • ShouldBindBodyWith* can read the body multiple times within one handler.

In the next episode, episode 6, we'll dissect middleware — writing your own middleware with c.Next() and c.Abort(), using built-in middleware like Logger, Recovery, BasicAuth, CORS, and gzip, and controlling the order of execution.