Skip to main content

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:

bash
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:

go
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 provided
  • email: Must be a valid email address
  • min, max: Minimum and maximum length for strings
  • gte, lte: Greater than or equal to, less than or equal to (for numbers)
  • oneof: Value must be one of the specified values
  • numeric: Must contain only numeric characters
  • alpha: Must contain only alphabetic characters
  • alphanum: Must contain only alphanumeric characters

Request Binding Methods

Gin provides several methods to bind request data to Go structs:

JSON Binding

go
// Bind JSON from request body
if err := c.ShouldBindJSON(&obj); err != nil {
// Handle error
}

Form Binding

go
// Bind form data
if err := c.ShouldBind(&obj); err != nil {
// Handle error
}

Query Binding

go
// Bind query parameters
if err := c.ShouldBindQuery(&obj); err != nil {
// Handle error
}

URL Path Parameter Binding

go
// 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:

go
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:

go
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:

go
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:

json
{
"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:

json
{
"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:

json
{
"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:

json
{
"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:

go
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:

  1. The importance of request validation in web applications
  2. How to use Gin's built-in validation capabilities
  3. Common validation tags and their usage
  4. Different methods to bind and validate request data
  5. Creating custom validation functions for complex requirements
  6. Handling validation errors appropriately
  7. 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

  1. Create a Gin API endpoint for user profile updates with validation for fields like name, email, age, and profile picture URL.
  2. Implement a custom validator that checks if a string contains no special characters except hyphens and underscores.
  3. Create an address validation system that checks zip codes against a specific country format.
  4. Implement file upload validation that checks file size and allowed file types.
  5. 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! :)