Gin Validation
Introduction
When building web applications and APIs with the Gin framework, validating incoming request data is a critical step in ensuring your application's security and reliability. Validation ensures that the data your application receives matches the expected format before any processing takes place.
In this tutorial, we'll explore how to implement request validation in Gin using the built-in binding capabilities and the popular go-playground/validator
package that Gin uses under the hood.
Why Validation Matters
Without proper validation:
- Your application might crash due to unexpected input data
- You risk security vulnerabilities such as injection attacks
- You waste resources processing invalid requests
- Your application might store corrupted or inconsistent data
Basic Validation with Gin
Setting Up
First, make sure you have the Gin framework installed:
go get -u github.com/gin-gonic/gin
Gin uses the go-playground/validator/v10
package internally, which is included when you install Gin.
Structure Validation Tags
Gin leverages Go struct field tags for validation. We define validation rules using the binding
tag.
Let's create a simple example for a user registration API:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// User defines the user registration data structure
type User struct {
Username string `json:"username" binding:"required,min=4,max=20"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,gte=18"`
Password string `json:"password" binding:"required,min=8"`
}
func main() {
r := gin.Default()
r.POST("/register", registerUser)
r.Run(":8080")
}
func registerUser(c *gin.Context) {
var newUser User
// This will validate the request body against our User struct
if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// If we get here, validation passed
c.JSON(http.StatusOK, gin.H{
"message": "User validation successful",
"user": newUser,
})
}
Common Validation Tags
Here are some commonly used validation tags:
required
: Field must be providedemail
: Must be a valid email addressmin
,max
: Minimum and maximum length for stringsgte
,lte
: Greater than or equal to, less than or equal to (for numbers)oneof
: Value must be one of the specified valuesnumeric
: Must contain only numeric charactersalpha
: Must contain only alphabetic charactersalphanum
: Must contain only alphanumeric characters
Request Binding Methods
Gin provides several methods to bind request data to Go structs:
JSON Binding
// Bind JSON from request body
if err := c.ShouldBindJSON(&obj); err != nil {
// Handle error
}
Form Binding
// Bind form data
if err := c.ShouldBind(&obj); err != nil {
// Handle error
}
Query Binding
// Bind query parameters
if err := c.ShouldBindQuery(&obj); err != nil {
// Handle error
}
URL Path Parameter Binding
// Bind URL parameters
if err := c.ShouldBindUri(&obj); err != nil {
// Handle error
}
Advanced Validation Techniques
Custom Validation Error Messages
To provide custom error messages, you can create a custom validator:
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
func registerHandler(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
var validationErrors []string
// Type assert to validator.ValidationErrors to get detailed information
if errs, ok := err.(validator.ValidationErrors); ok {
for _, e := range errs {
// Create custom error message based on the failed validation
switch e.Field() {
case "Username":
validationErrors = append(validationErrors, "Username is required")
case "Email":
if e.Tag() == "required" {
validationErrors = append(validationErrors, "Email is required")
} else {
validationErrors = append(validationErrors, "Email format is invalid")
}
}
}
}
c.JSON(http.StatusBadRequest, gin.H{
"errors": validationErrors,
})
return
}
// Process valid request...
c.JSON(http.StatusOK, gin.H{"message": "Registration successful"})
}
Custom Validation Functions
You can also create custom validation functions for complex validation rules:
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"net/http"
"regexp"
"strings"
)
// PasswordPolicy defines password complexity requirements
type PasswordPolicy struct {
Password string `json:"password" binding:"required,strongPassword"`
}
// Validate that a password has at least one uppercase, lowercase, number and special char
func validateStrongPassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[^A-Za-z0-9]`).MatchString(password)
return len(password) >= 8 && hasUpper && hasLower && hasNumber && hasSpecial
}
func main() {
r := gin.Default()
// Register custom validator
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("strongPassword", validateStrongPassword)
}
r.POST("/validate-password", func(c *gin.Context) {
var policy PasswordPolicy
if err := c.ShouldBindJSON(&policy); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Password must be at least 8 characters and include uppercase, lowercase, number, and special characters",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Password meets security requirements",
})
})
r.Run(":8080")
}
Real-World Example: Product API with Validation
Let's implement a more comprehensive example for a product API with validation:
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"net/http"
"time"
)
// Product defines the data structure for product information
type Product struct {
ID string `json:"id" binding:"required,uuid"`
Name string `json:"name" binding:"required,min=3,max=100"`
Description string `json:"description" binding:"max=1000"`
Price float64 `json:"price" binding:"required,gt=0"`
Category string `json:"category" binding:"required,oneof=electronics clothing food furniture"`
InStock bool `json:"in_stock"`
CreatedAt time.Time `json:"created_at" binding:"required,ltefield=UpdatedAt"`
UpdatedAt time.Time `json:"updated_at" binding:"required"`
SKU string `json:"sku" binding:"required,sku"`
}
// validateSKU checks if a SKU follows our company format (XX-YYYYY)
func validateSKU(fl validator.FieldLevel) bool {
sku := fl.Field().String()
// Check format with regex (2 letters, hyphen, 5 digits)
match, _ := regexp.MatchString(`^[A-Z]{2}-\d{5}$`, sku)
return match
}
func main() {
r := gin.Default()
// Register custom validator for SKU
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("sku", validateSKU)
}
r.POST("/products", createProduct)
r.Run(":8080")
}
func createProduct(c *gin.Context) {
var product Product
if err := c.ShouldBindJSON(&product); err != nil {
errors := []string{}
// Extract detailed validation errors
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, e := range validationErrors {
switch e.Field() {
case "ID":
errors = append(errors, "Product ID must be a valid UUID")
case "Name":
errors = append(errors, "Product name must be between 3 and 100 characters")
case "Price":
errors = append(errors, "Price must be greater than zero")
case "Category":
errors = append(errors, "Category must be one of: electronics, clothing, food, furniture")
case "SKU":
errors = append(errors, "SKU format must be XX-YYYYY (2 uppercase letters, hyphen, 5 digits)")
case "CreatedAt":
errors = append(errors, "Created date must be before or equal to Updated date")
default:
errors = append(errors, e.Error())
}
}
}
c.JSON(http.StatusBadRequest, gin.H{
"errors": errors,
})
return
}
// Process the validated product
// In a real app, you would save to database here
c.JSON(http.StatusCreated, gin.H{
"message": "Product created successfully",
"product": product,
})
}
Testing the Validation
Let's test with invalid data:
Request:
{
"id": "not-a-uuid",
"name": "TV",
"price": -100,
"category": "invalid-category",
"sku": "invalid-sku",
"created_at": "2023-04-01T10:00:00Z",
"updated_at": "2023-03-01T10:00:00Z"
}
Response:
{
"errors": [
"Product ID must be a valid UUID",
"Product name must be between 3 and 100 characters",
"Price must be greater than zero",
"Category must be one of: electronics, clothing, food, furniture",
"SKU format must be XX-YYYYY (2 uppercase letters, hyphen, 5 digits)",
"Created date must be before or equal to Updated date"
]
}
Request with valid data:
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Smart Television",
"description": "4K Ultra HD Smart TV with voice control",
"price": 599.99,
"category": "electronics",
"in_stock": true,
"created_at": "2023-03-01T10:00:00Z",
"updated_at": "2023-04-01T10:00:00Z",
"sku": "EL-12345"
}
Response:
{
"message": "Product created successfully",
"product": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Smart Television",
"description": "4K Ultra HD Smart TV with voice control",
"price": 599.99,
"category": "electronics",
"in_stock": true,
"created_at": "2023-03-01T10:00:00Z",
"updated_at": "2023-04-01T10:00:00Z",
"sku": "EL-12345"
}
}
Handling Different Types of Validation Failures
It's important to handle different types of validation failures appropriately:
func handleValidation(c *gin.Context, obj interface{}) bool {
// Try to bind and validate
if err := c.ShouldBindJSON(&obj); err != nil {
// Different handling based on error type
if _, ok := err.(*json.SyntaxError); ok {
// Handle JSON syntax errors
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid JSON format",
})
return false
}
if validationErrors, ok := err.(validator.ValidationErrors); ok {
// Handle validation errors
errors := make(map[string]string)
for _, e := range validationErrors {
errors[e.Field()] = buildErrorMsg(e)
}
c.JSON(http.StatusBadRequest, gin.H{
"error": "Validation failed",
"details": errors,
})
return false
}
// Handle other errors
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return false
}
return true
}
// Helper to build error messages
func buildErrorMsg(e validator.FieldError) string {
switch e.Tag() {
case "required":
return "This field is required"
case "email":
return "Must be a valid email address"
case "min":
return "Value is too short"
case "max":
return "Value is too long"
default:
return "Invalid value"
}
}
Summary
In this tutorial, we've covered:
- The importance of request validation in web applications
- How to use Gin's built-in validation capabilities
- Common validation tags and their usage
- Different methods to bind and validate request data
- Creating custom validation functions for complex requirements
- Handling validation errors appropriately
- Building user-friendly error messages
Implementing proper validation in your Gin applications will ensure your APIs are robust, secure, and provide a better user experience by catching errors early and providing helpful feedback.
Exercises
- Create a Gin API endpoint for user profile updates with validation for fields like name, email, age, and profile picture URL.
- Implement a custom validator that checks if a string contains no special characters except hyphens and underscores.
- Create an address validation system that checks zip codes against a specific country format.
- Implement file upload validation that checks file size and allowed file types.
- Build a form with multiple nested objects and validate all fields appropriately.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)