Go/Gin

Request Validation in Gin: Binding Tags vs Custom Validators

5 min read by DebuggedIt

Quick answer

Quick answer: use binding tags for simple structural checks, such as required fields, formats, and ranges. Write custom validators for reusable rules that tags...

Quick answer: use binding tags for simple structural checks, such as required fields, formats, and ranges. Write custom validators for reusable rules that tags cannot express, and keep business rules in the service layer. Use ShouldBind methods so you control the error response, and use pointer fields when you must tell a missing value from a zero value.

What Can Binding Tags Validate in Gin?

Gin integrates a validation library that reads the binding struct tag. You declare rules next to the fields, and Gin checks them when it binds the request body, query, or form. Tags cover common cases: required, minimum and maximum values or lengths, email and URL formats, one-of lists, and basic comparisons.

This is the most maintainable option for structural validation, because the rules live with the data shape and are visible to anyone reading the struct. It also keeps handlers short. The exact set of tags depends on the validation library version bundled with your Gin version, so check the documentation for the available rules.

Be careful with the required tag and zero values. A required integer rejects 0, and a required boolean rejects false, because they equal the zero value. If zero is a legitimate input, use a pointer field so a missing value is nil and an explicit zero is accepted.

type CreateUserRequest struct {
    Email string  `json:"email" binding:"required,email"`
    Age   *int    `json:"age" binding:"required,gte=0,lte=130"`
    Role  string  `json:"role" binding:"required,oneof=admin member viewer"`
}

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

When Do You Need Custom Validators?

Tags run out when a rule is specific to your domain or involves several fields. Examples include a password that must meet your policy, a slug format, a start date that must precede an end date, or a country-specific identifier. For these, you can register a custom validation function under a tag name and reuse it across structs.

Cross-field rules can be implemented with struct-level validation, which receives the whole struct and reports errors on specific fields. It keeps related checks together, and you avoid scattering conditional logic through handlers.

  • Use custom field validators for formats and policies that repeat across requests.
  • Use struct-level validation for rules that compare fields to each other.
  • Use plain Go code in the handler or service for rules that need external data.

Avoid putting database lookups inside validators. A validator that checks whether an email already exists makes validation slow, couples it to storage, and creates race conditions, since another request can insert the same email right after the check. Enforce uniqueness with a database constraint and translate the conflict error.

Where Do Business Rules and Error Messages Belong?

Separate shape from meaning. Binding validation answers whether the request is well formed, and the service layer answers whether the operation is allowed. Whether a coupon is still valid or a user can change plans belongs in the service, where it can be tested without HTTP and reused by other entry points.

Prefer the ShouldBind family over the Bind family. The Bind methods abort the request and write a 400 automatically, which leaves you little control over the format. ShouldBind returns the error so you can build a consistent response with field-level details, using a helper that converts validation errors into a client-friendly list.

Also consider rejecting unknown fields in JSON bodies for stricter APIs. Gin exposes a setting for this on its binding package, but check the documentation for your version, because behavior and naming have changed across releases.

Frequently Asked Questions

What is the difference between Bind and ShouldBind in Gin?

Bind methods abort the request with a 400 status automatically on error. ShouldBind methods only return the error, so you decide how to respond, which is better for consistent API errors.

Why does required reject my zero value?

The required rule treats the zero value of a type as missing. Use a pointer field if zero is valid, so the validator can distinguish absent from zero.

Can I validate query parameters and headers too?

Yes. Gin can bind query strings, forms, and headers into structs with matching tags, and the same binding rules apply. Check the documentation for the tag names.

Should I return all validation errors or just the first?

Returning all field errors gives clients a better experience, since they can fix everything at once. Convert the validator's error list into a structured response.

Summary

Use binding tags for structural rules, custom and struct-level validators for reusable domain checks, and the service layer for business logic. Prefer ShouldBind for control over responses, use pointers for optional zero values, and leave uniqueness to the database. Confirm available tags and options in the Gin documentation for your version.