Go/Gin

Go Gin Binding and Validation Tags for Request Payloads

5 min read by DebuggedIt

Quick answer

Gin binds request data into Go structs using struct tags, and validates it with the validator library through the binding tag. Prefer the ShouldBind family...

Gin binds request data into Go structs using struct tags, and validates it with the validator library through the binding tag. Prefer the ShouldBind family over Bind, because ShouldBind returns the error and lets you control the response, while Bind writes a 400 status automatically. Good validation catches malformed input at the boundary and returns clear, structured errors.

How Does Gin Binding Work?

Gin can bind JSON bodies, query strings, form data, URI parameters, and headers into a struct. You describe the fields with tags such as json, form, uri, and binding, and call a method that matches the data source.

type CreateUserRequest struct {
    Email string `json:"email" binding:"required,email"`
    Name  string `json:"name"  binding:"required,min=2,max=80"`
    Age   int    `json:"age"   binding:"omitempty,gte=13,lte=120"`
}

func (h *Handler) CreateUser(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
        return
    }
    // use req
}

ShouldBindJSON, ShouldBindQuery, ShouldBindUri, and ShouldBind each handle one source or pick one from the content type. The Must-style methods such as BindJSON abort the request with a 400 and set headers, which leaves you less control over the error format.

Binding and validation happen in one step, but they are conceptually separate. First the body is decoded into the struct, which can fail on malformed JSON or type mismatches such as a string where a number was expected. Then the validator checks the tags, which can fail on rule violations. Treating the two failures differently gives clients better feedback.

Which Validation Tags Are Most Useful?

Gin uses the go-playground validator, which offers a large set of tags. A small subset covers most API needs.

  • required: the field must be present and not the zero value. Remember that zero values such as 0 and false count as missing.
  • omitempty: skip other rules when the field is empty.
  • min, max, len, gte, lte: bounds for strings, numbers, and collections.
  • oneof: restrict the value to a fixed set.
  • email, url, uuid: format checks.
  • dive: apply rules to elements of slices and maps.

The zero value issue deserves attention. If a boolean or integer field can legitimately be false or zero, using required rejects valid input. Use a pointer type, such as a pointer to bool, so that absent and false can be told apart.

How Do You Return Useful Validation Errors?

The default error message is a long string that exposes internal field names and tags, which is not friendly to API clients. Convert validation errors into a stable structure that lists the failing fields.

var ve validator.ValidationErrors
if errors.As(err, &ve) {
    out := make([]gin.H, 0, len(ve))
    for _, fe := range ve {
        out = append(out, gin.H{"field": fe.Field(), "rule": fe.Tag()})
    }
    c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": out})
    return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "malformed body"})

Distinguish between malformed input, such as invalid JSON, and valid JSON that fails rules. Some teams use 400 for the first and 422 for the second. Whatever you choose, apply it consistently and document it. By default the field name reported follows the struct field, so register a tag name function if you want clients to see the JSON names.

When Do You Need Custom Validators?

Built-in tags cannot express every rule. Cross-field checks, such as an end date after a start date, and domain rules, such as an allowed currency code, call for custom validators or explicit checks in code.

Register custom validation functions with the validator engine used by Gin, and make sure the version of the validator package your Gin version uses supports what you need, since APIs differ between releases. For rules that depend on the database, such as uniqueness, do the check in the service layer. Struct tags should validate shape, not business state.

  • Use tags for syntactic rules: presence, length, format, and ranges.
  • Use service code for rules that need context or data lookups.
  • Limit request body size so validation is not the only protection against huge payloads.
  • Reject unknown fields if strictness matters, using the JSON decoder options where available.

Keep request structs separate from database models. A dedicated request type lets you accept only the fields clients should control, which prevents mass assignment problems where a client sets internal fields such as role or id by including them in the payload.

Frequently Asked Questions

What is the difference between Bind and ShouldBind?

Bind aborts with a 400 response automatically. ShouldBind returns the error and leaves the response to you, which is usually what you want.

Why does required fail for zero or false?

The validator treats zero values as missing. Use pointer fields when zero and false are valid inputs.

Can I validate query parameters the same way?

Yes. Use form tags with ShouldBindQuery and apply the same binding tags.

Should uniqueness be checked with validation tags?

No. Check it in the service layer and rely on a database unique constraint as the final guard.

Summary

Use ShouldBind with binding tags for structural validation, convert errors into a clean format, watch for zero value pitfalls, and keep business rules out of tags. Check the Gin and validator documentation for your versions.