Robust Request Validation with Gin and Validator v10
James Reed
Infrastructure Engineer · Leapcell

Introduction: The Unseen Guard of Reliable APIs
In the realm of modern web development, APIs serve as the crucial gateways through which applications interact and exchange data. The health and reliability of an API are not merely judged by its speed or functionality, but fundamentally by the integrity of the data it processes. Mismatched or malformed input data can lead to a cascade of errors, ranging from incorrect database entries and unexpected application behavior to security vulnerabilities. Therefore, robust request data validation is not just a best practice; it's an indispensable component of building resilient and trustworthy backend services.
While frameworks like Gin provide excellent tools for routing and handling HTTP requests, they typically don't offer out-of-the-box, comprehensive data validation capabilities. This is where dedicated validation libraries shine, integrating seamlessly to ensure that incoming data adheres to predefined rules before business logic is ever touched. This article delves into how we can leverage the powerful combination of Gin and the widely-used go‐playground/validator
v10 library in Go to construct elaborate and flexible request data validation logic, thereby safeguarding our applications and enhancing API quality.
Core Concepts and Implementation Details
Before we dive into the practical implementation, let's briefly clarify some core concepts that are central to our discussion:
- Gin: A high-performance HTTP web framework written in Go. It's known for its speed and minimalistic design, providing essential features for building web applications and APIs.
- Struct Tag: Metadata attached to struct fields in Go, typically used by reflection. In the context of
validator
, struct tags define the validation rules for each field. go-playground/validator
(v10): A Go struct and field validation library that supports features like cross-field validation, custom validators, and internationalization. It's highly configurable and efficient.- Custom Validation: The ability to define and register your own validation rules that go beyond the built-in ones provided by the
validator
library. This is crucial for domain-specific business logic. - Cross-Field Validation: Validation rules that depend on the values of multiple fields within the same structure. For example, ensuring a
confirm_password
field matches thepassword
field.
Setting Up the Environment
First, ensure you have Gin and Validator v10 installed:
go get github.com/gin-gonic/gin go get github.com/go-playground/validator/v10
Basic Request Validation
Let's start with a simple example of validating user registration data.
package main import ( "log" "net/http" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" ) // UserRegisterRequest defines the structure for user registration data type UserRegisterRequest struct { Username string `json:"username" binding:"required,min=3,max=30"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` } func main() { r := gin.Default() // Register a handler for user registration r.POST("/register", func(c *gin.Context) { var req UserRegisterRequest if err := c.ShouldBindJSON(&req); err != nil { // Handle validation errors var validationErrors validator.ValidationErrors if ok := c.Error.IsType(&validationErrors); ok { errorResponse := make(map[string]string) for _, fieldErr := range validationErrors { errorResponse[fieldErr.Field()] = fieldErr.Tag() } c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": errorResponse}) } else { // Handle other binding errors (e.g., malformed JSON) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } return } // If validation passes, process the request c.JSON(http.StatusOK, gin.H{"message": "User registered successfully", "user": req.Username}) }) log.Fatal(r.Run(":8080")) // Listen and serve on 0.0.8080 }
In this example:
- We define a
UserRegisterRequest
struct withjson
tags for JSON unmarshalling andbinding
tags for validation. binding:"required,min=3,max=30"
means theUsername
field is mandatory, must be at least 3 characters long, and at most 30 characters.binding:"required,email"
ensuresEmail
is mandatory and a valid email format.c.ShouldBindJSON(&req)
attempts to bind the request body to our struct and automatically runs the validations defined by thebinding
tags.- Error handling differentiates between validation errors (
validator.ValidationErrors
) and other binding errors.
Integrating Custom Validation Rules
Sometimes, built-in validators aren't enough. Let's say we have a requirement that a username cannot be "admin" or "root". We can achieve this with a custom validator.
package main import ( "log" "net/http" "reflect" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" ) // Create a global validator instance var validate *validator.Validate // isNotReserved checks if a string is not a reserved username func isNotReserved(fl validator.FieldLevel) bool { reservedUsernames := map[string]bool{ "admin": true, "root": true, } return !reservedUsernames[fl.Field().String()] } // UserRegisterRequest with custom validation type UserRegisterRequest struct { Username string `json:"username" binding:"required,min=3,max=30,notreserved"` // Added 'notreserved' Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` } func main() { r := gin.Default() // Initialize the validator and register custom validation validate = validator.New() validate.RegisterValidation(`notreserved`, isNotReserved) // Custom type converter for binding to use our global validator instance if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("notreserved", isNotReserved) } r.POST("/register", func(c *gin.Context) { var req UserRegisterRequest if err := c.ShouldBindJSON(&req); err != nil { var validationErrors validator.ValidationErrors if ok := c.Error.IsType(&validationErrors); ok { errorResponse := make(map[string]string) for _, fieldErr := range validationErrors { errorResponse[fieldErr.Field()] = fieldErr.Tag() } c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": errorResponse}) } else { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "User registered successfully", "user": req.Username}) }) log.Fatal(r.Run(":8080")) }
Here, we've introduced notreserved
as a custom validation rule:
isNotReserved
is a function that takesvalidator.FieldLevel
and returnstrue
if the field value is valid,false
otherwise.validate.RegisterValidation("notreserved", isNotReserved)
registers this function with our validator instance.- We ensure Gin's
binding
engine uses our custom validator by checking its type and registering our custom rule. A more robust way to integrate a custom validator gracefully with Gin is to replace Gin's default validator.
Replacing Gin's Default Validator with a Custom One
For more control, especially with custom tags, it's best to replace Gin's default validator.
package main import ( "log" "net/http" "reflect" "strings" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" ) // CustomValidator implements gin.Binding.Validator interface type CustomValidator struct { validator *validator.Validate } func (cv *CustomValidator) ValidateStruct(obj interface{}) error { if kind := reflect.TypeOf(obj).Kind(); kind == reflect.Ptr { obj = reflect.ValueOf(obj).Elem().Interface() } return cv.validator.Struct(obj) } func (cv *CustomValidator) Engine() interface{} { return cv.validator } // isNotReserved remains the same func isNotReserved(fl validator.FieldLevel) bool { reservedUsernames := map[string]bool{ "admin": true, "root": true, } return !reservedUsernames[fl.Field().String()] } // PasswordMatch checks if passwords match func PasswordMatch(fl validator.FieldLevel) bool { // Get the whole struct req := fl.Top().Interface().(UserRegisterRequest) return req.Password == req.ConfirmPassword } // UserRegisterRequest with custom and cross-field validation type UserRegisterRequest struct { Username string `json:"username" binding:"required,min=3,max=30,notreserved"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password"` // Cross-field validation } func main() { r := gin.Default() // Initialize our custom validator and register rules validate := validator.New() validate.RegisterValidation("notreserved", isNotReserved) // Optionally, register a custom tag for cross-field validation if `eqfield` isn't expressive enough // validate.RegisterValidation("passwordmatch", PasswordMatch) // This could be an alternative to `eqfield` // Register field name tag func to use json tag as field names in errors validate.RegisterTagNameFunc(func(fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] if name == "-" { return "" } return name }) // Replace Gin's default validator binding.Validator = &CustomValidator{validator: validate} r.POST("/register", func(c *gin.Context) { var req UserRegisterRequest if err := c.ShouldBindJSON(&req); err != nil { if validationErrors, ok := err.(validator.ValidationErrors); ok { errorResponse := make(map[string]string) for _, fieldErr := range validationErrors { // Use the custom field name from json tag errorResponse[fieldErr.Field()] = fieldErr.Tag() } c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed", "details": errorResponse}) } else { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": "User registered successfully", "user": req.Username}) }) log.Fatal(r.Run(":8080")) }
Notable changes and advanced features in this version:
CustomValidator
struct: Implementsgin.Binding.Validator
interface, allowing us to completely control the validation process with ourvalidator
instance.binding.Validator = &CustomValidator{validator: validate}
: This line replaces Gin's default validator with our custom one.eqfield=Password
: This is a built-in cross-field validation rule fromvalidator
v10. It ensures thatConfirmPassword
is equal to thePassword
field.validate.RegisterTagNameFunc
: This is a powerful feature that allows us to map field names in validation errors. By default,validator
uses the struct field name (e.g.,Username
). We're telling it to use thejson
tag name (e.g.,username
) instead, which generally leads to more user-friendly error messages when dealing with JSON APIs.
Application Scenarios
This combination of Gin and validator
v10 is ideal for a wide array of scenarios:
- User Authentication and Authorization: Validating email formats, password strengths, and ensuring unique usernames.
- E-commerce Platforms: Checking product quantities, valid addresses, and payment information formats.
- APIs for IoT devices: Ensuring data from devices adheres to specific protocols and formats.
- Configuration APIs: Validating complex configuration structures before applying them.
- Form Submissions: Any web form requiring structured and validated input.
The ability to create custom validators allows developers to encapsulate complex business logic validations directly within their DTO (Data Transfer Object) structs, making the code cleaner, more maintainable, and easier to understand.
Conclusion: Fortifying Your API with Intelligent Validation
By deeply integrating go-playground/validator
v10 with the Gin web framework, developers can construct sophisticated and highly customizable request data validation logic. This not only preemptively tackles malformed or invalid input, preventing cascading failures and ensuring data integrity, but also significantly improves the robustness, security, and overall reliability of your APIs. Intelligent validation is the unseen guardian of a stable application, turning potential chaos into predictable order.