How to Validate Request Bodies with Custom Validators in Gin
Gin validates request bodies through its binding package by unmarshaling data into structs and passing them to a global Validator instance that wraps go-playground/validator/v10, which you can extend by casting binding.Validator.Engine() to *validator.Validate and registering custom validation functions.
Gin provides a robust validation pipeline for HTTP request bodies through its integration with the go-playground/validator library. Understanding how to validate request bodies with custom validators in Gin requires knowledge of the framework's binding architecture and the exposed validator engine. This guide examines the source code implementation in the gin-gonic/gin repository to show you how to register field-level and struct-level validation rules while leveraging the framework's automatic validation triggers.
Understanding the Validation Pipeline
Gin's validation workflow follows a strict sequence from request binding to struct validation. When a handler calls c.ShouldBind(&obj) or any of its variants, the framework selects the appropriate Binding implementation based on the request's Content-Type (see the Default function in binding/binding.go lines 93-120).
After the raw payload is unmarshaled into your struct, Gin invokes the private validate helper function (lines 22-27 of binding/binding.go):
func validate(obj any) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}
This helper forwards the struct to the global Validator variable, which must implement the StructValidator interface defined in lines 51-68 of the same file.
Accessing the Underlying Validator Engine
By default, binding.Validator is an instance of defaultValidator declared at line 72 of binding/binding.go. This implementation wraps go-playground/validator/v10 and exposes the underlying engine through the Engine() method (lines 81-88 of binding/default_validator.go):
func (v *defaultValidator) Engine() any {
v.lazyinit()
return v.validate
}
The engine is created lazily using sync.Once (lines 90-95 of binding/default_validator.go) with the tag name set to "binding". To register custom validators, you must cast this engine to *validator.Validate:
import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Register custom validations here
_ = v.RegisterValidation("custom_tag", myCustomFunc)
}
}
Creating Custom Field Validators
For single-field validation logic, use the RegisterValidation method. This approach is ideal for checking password complexity, custom string formats, or business-specific rules:
import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"strings"
)
// passwordValidator ensures the field contains at least one digit and one special character
func passwordValidator(fl validator.FieldLevel) bool {
pwd := fl.Field().String()
hasDigit := strings.ContainsAny(pwd, "0123456789")
hasSpecial := strings.ContainsAny(pwd, "!@#$%^&*")
return hasDigit && hasSpecial
}
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("pwd", passwordValidator)
}
}
Apply the custom tag in your struct definition:
type SignUpForm struct {
Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,pwd,min=8"`
}
When Gin processes the request via c.ShouldBindJSON(&form), the custom pwd validator executes automatically alongside the built-in rules.
Implementing Cross-Field Validation
For validation logic that compares multiple fields (such as password confirmation), use RegisterStructValidation. This method receives the entire struct and can report errors on specific fields:
type LoginForm struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
ConfirmPassword string `json:"confirm_password" binding:"required"`
}
func loginStructValidator(sl validator.StructLevel) {
login := sl.Current().Interface().(LoginForm)
if login.Password != login.ConfirmPassword {
sl.ReportError(login.ConfirmPassword, "ConfirmPassword", "ConfirmPassword", "pwd_mismatch", "")
}
}
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterStructValidation(loginStructValidator, LoginForm{})
}
}
The ReportError method allows you to attach validation failures to specific struct fields while maintaining the standard Gin error response format.
Key Source Files and Implementation Details
The validation pipeline spans several critical files in the gin-gonic/gin repository:
binding/binding.go(lines 29-36): Defines theBindinginterface withBind(*http.Request, any) errorfor each content type (JSON, XML, Form, etc.)binding/binding.go(lines 51-68): Declares theStructValidatorinterface requiringValidateStruct(any) errorandEngine() anybinding/default_validator.go(lines 43-70): ImplementsValidateStructwith support for structs, pointers, and slices/arrays, including aggregation of slice element errorsbinding/default_validator.go(lines 81-88): Exposes the underlying*validator.Validateengine for custom rule registrationcontext.go: Provides*gin.Contextmethods (ShouldBind,ShouldBindJSON,ShouldBindQuery) that orchestrate the binding and validation flow
The defaultValidator handles type resolution automatically, validating pointers by dereferencing them and validating slices by iterating through elements, ensuring consistent behavior across complex nested structures.
Summary
- Gin uses a global
Validatorvariable that implements theStructValidatorinterface to validate request bodies after binding - The default implementation wraps go-playground/validator/v10 and exposes the engine via
Engine(), allowing you to register custom validation functions without modifying the framework core - Cast
binding.Validator.Engine()to*validator.Validateto accessRegisterValidationfor field-level rules orRegisterStructValidationfor cross-field logic - Validation triggers automatically when calling
c.ShouldBind(),c.ShouldBindJSON(), or other binding methods on the Gin context - Register custom validators in an
init()function to ensure they are available before the server handles requests
Frequently Asked Questions
How do I replace the default validator entirely in Gin?
Set the global binding.Validator variable to your own implementation of the StructValidator interface before starting your server. Your implementation must provide ValidateStruct(any) error and Engine() any methods. This completely overrides the default go-playground/validator integration with your custom logic.
Can I use validation tags other than "binding" in struct tags?
The default validator engine is configured with the tag name "binding" as seen in binding/default_validator.go line 93. While you cannot change this tag name without replacing the entire validator, you can register multiple custom validation functions under different tag names (like pwd or isodate) and use them within the binding tag string separated by commas.
How does Gin handle validation errors in slices or arrays?
According to the ValidateStruct implementation in binding/default_validator.go lines 43-70, Gin iterates through slice and array elements individually, validating each one and collecting errors. This ensures that every item in a request body array receives validation, not just the container itself.
Where should I register custom validators to ensure they load before requests arrive?
Register custom validators in an init() function within a package imported by your main application. Since Gin initializes the validator engine lazily using sync.Once (lines 90-95 of binding/default_validator.go), calling binding.Validator.Engine() in your init() triggers the initialization immediately, ensuring your custom rules are registered before the first request reaches a handler.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →