Skip to main content

Gin Custom Validators

Introduction

When building web applications with Gin, validating incoming data is a critical step to ensure your application processes only valid and correctly formatted information. While Gin integrates with the popular validator package to provide basic validation capabilities, you'll often need custom validation logic that goes beyond the standard rules.

Custom validators allow you to define specific validation rules tailored to your application's business requirements. This enables you to:

  • Validate data according to complex business logic
  • Create domain-specific validation rules
  • Extend Gin's validation capabilities for unique use cases
  • Provide better, more specific error messages to API users

In this tutorial, we'll explore how to create and implement custom validators in your Gin applications.

Understanding Validation in Gin

Before diving into custom validators, let's review how validation works in Gin. By default, Gin uses the go-playground/validator package for validations through struct tags.

Here's a quick example of built-in validation:

go
type UserRequest struct {
Username string `json:"username" binding:"required,min=3,max=30"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,gte=18"`
}

func registerHandler(c *gin.Context) {
var user UserRequest
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Process valid user data
c.JSON(http.StatusOK, gin.H{"message": "User registered successfully"})
}

While this works well for basic validation, sometimes you need more complex rules.

Creating Custom Validators

Step 1: Understanding the Validator Interface

To create custom validators in Gin, you need to work with the underlying validator engine. This involves registering custom validation functions with the validator package.

The first step is to access the validator engine from Gin:

go
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)

func main() {
router := gin.Default()

// Access the validator engine
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Register custom validators here
}

// Routes and other configurations
router.Run(":8080")
}

Step 2: Registering a Custom Validator

Let's create a custom validator that checks if a string is a valid product code in our system. A valid product code might start with "PRD-" followed by digits:

go
func main() {
router := gin.Default()

// Access the validator engine
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Register custom validator
v.RegisterValidation("product_code", validateProductCode)
}

// Set up routes
router.POST("/products", addProductHandler)

router.Run(":8080")
}

// Custom validation function
func validateProductCode(fl validator.FieldLevel) bool {
code := fl.Field().String()
// Check if the code starts with "PRD-" and is followed by digits
return len(code) >= 5 && strings.HasPrefix(code, "PRD-") && isNumeric(code[4:])
}

func isNumeric(s string) bool {
for _, char := range s {
if char < '0' || char > '9' {
return false
}
}
return true
}

Step 3: Using the Custom Validator in Your Struct

Once registered, you can use your custom validator in struct tags:

go
type Product struct {
Name string `json:"name" binding:"required"`
Code string `json:"code" binding:"required,product_code"`
Price float64 `json:"price" binding:"required,gt=0"`
Description string `json:"description" binding:"max=500"`
}

func addProductHandler(c *gin.Context) {
var product Product
if err := c.ShouldBindJSON(&product); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Process the valid product
c.JSON(http.StatusOK, gin.H{"message": "Product added successfully", "product": product})
}

Custom Validators with Parameters

Sometimes you need validators that accept parameters. Let's create a more flexible validator that checks if a string is in a specific format with a configurable prefix:

Step 1: Register a Custom Validator with Parameters

go
func main() {
router := gin.Default()

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Register custom validator with parameters
v.RegisterValidation("prefix", validatePrefixFormat)
}

router.POST("/items", addItemHandler)
router.Run(":8080")
}

// Custom validation function that accepts parameters
func validatePrefixFormat(fl validator.FieldLevel) bool {
// Get parameter from the tag
param := fl.Param()
value := fl.Field().String()

// Value must start with the specified prefix
return strings.HasPrefix(value, param)
}

Step 2: Using the Parameterized Validator

go
type Item struct {
ID string `json:"id" binding:"required,prefix=ITM-"`
Category string `json:"category" binding:"required,prefix=CAT-"`
Quantity int `json:"quantity" binding:"required,gte=1"`
}

func addItemHandler(c *gin.Context) {
var item Item
if err := c.ShouldBindJSON(&item); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Item added successfully", "item": item})
}

Real-World Example: Complex Business Validation

Let's explore a more comprehensive example that demonstrates how custom validators can be used to implement complex business rules.

Imagine we're building an e-commerce API that needs to validate several aspects of an order:

  1. Shipping dates must be in the future but not more than 30 days ahead
  2. Promo codes must follow a specific format and be valid in our system
  3. The order total must match the sum of item prices

Step 1: Define Custom Validators

go
func main() {
router := gin.Default()

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Register custom validators
v.RegisterValidation("future_date", validateFutureDate)
v.RegisterValidation("valid_promo", validatePromoCode)
v.RegisterValidation("check_total", validateOrderTotal)
}

router.POST("/orders", createOrderHandler)
router.Run(":8080")
}

// Validates that a date is in the future but not more than 30 days ahead
func validateFutureDate(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if !ok {
return false
}

now := time.Now()
maxDate := now.AddDate(0, 0, 30)

return date.After(now) && date.Before(maxDate)
}

// Validates a promo code format and checks if it exists
func validatePromoCode(fl validator.FieldLevel) bool {
code := fl.Field().String()

// Skip validation if empty (assuming promo codes are optional)
if code == "" {
return true
}

// Check format: PROMO-XXX where X are alphanumeric
if !regexp.MustCompile(`^PROMO-[A-Z0-9]{3}$`).MatchString(code) {
return false
}

// In a real app, you would check against valid codes in database
validCodes := []string{"PROMO-123", "PROMO-ABC", "PROMO-XYZ"}
for _, validCode := range validCodes {
if validCode == code {
return true
}
}

return false
}

// Validates that the order total matches sum of items
func validateOrderTotal(fl validator.FieldLevel) bool {
// This is a struct-level validator, so we need to access the parent struct
orderStruct := fl.Parent().Interface().(Order)

calculatedTotal := 0.0
for _, item := range orderStruct.Items {
calculatedTotal += item.Price * float64(item.Quantity)
}

// Allow for minor floating point differences (optional)
return math.Abs(orderStruct.Total - calculatedTotal) < 0.01
}

Step 2: Define the Order Struct with Custom Validations

go
type OrderItem struct {
ProductID string `json:"product_id" binding:"required"`
Quantity int `json:"quantity" binding:"required,min=1"`
Price float64 `json:"price" binding:"required,gt=0"`
}

type Order struct {
CustomerID string `json:"customer_id" binding:"required"`
Items []OrderItem `json:"items" binding:"required,dive"`
ShippingDate time.Time `json:"shipping_date" binding:"required,future_date"`
PromoCode string `json:"promo_code" binding:"omitempty,valid_promo"`
Total float64 `json:"total" binding:"required,gt=0,check_total"`
}

func createOrderHandler(c *gin.Context) {
var order Order
if err := c.ShouldBindJSON(&order); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Process the validated order
c.JSON(http.StatusOK, gin.H{
"message": "Order created successfully",
"order_id": "ORD-" + uuid.New().String(),
"order": order,
})
}

Example Input and Output

Input (Valid Order):

json
{
"customer_id": "CUST-12345",
"items": [
{
"product_id": "PRD-001",
"quantity": 2,
"price": 29.99
},
{
"product_id": "PRD-002",
"quantity": 1,
"price": 49.99
}
],
"shipping_date": "2023-12-15T00:00:00Z",
"promo_code": "PROMO-123",
"total": 109.97
}

Output (Success):

json
{
"message": "Order created successfully",
"order_id": "ORD-6b4f3a7c-8dfa-4b1e-9a3d-3f7c834572a1",
"order": {
"customer_id": "CUST-12345",
"items": [
{
"product_id": "PRD-001",
"quantity": 2,
"price": 29.99
},
{
"product_id": "PRD-002",
"quantity": 1,
"price": 49.99
}
],
"shipping_date": "2023-12-15T00:00:00Z",
"promo_code": "PROMO-123",
"total": 109.97
}
}

Input (Invalid Order):

json
{
"customer_id": "CUST-12345",
"items": [
{
"product_id": "PRD-001",
"quantity": 2,
"price": 29.99
}
],
"shipping_date": "2024-02-15T00:00:00Z",
"promo_code": "INVALID-PROMO",
"total": 50.00
}

Output (Error):

json
{
"error": "Key: 'Order.shipping_date' Error:Field validation for 'shipping_date' failed on the 'future_date' tag\nKey: 'Order.promo_code' Error:Field validation for 'promo_code' failed on the 'valid_promo' tag\nKey: 'Order.total' Error:Field validation for 'total' failed on the 'check_total' tag"
}

Providing Better Error Messages

The default error messages from the validator might not be user-friendly. Let's improve them:

go
func createOrderHandler(c *gin.Context) {
var order Order
if err := c.ShouldBindJSON(&order); err != nil {
// Extract validation errors
var friendlyErrors []string

if errs, ok := err.(validator.ValidationErrors); ok {
for _, e := range errs {
// Customize messages based on the field and tag
switch {
case e.Field() == "ShippingDate" && e.Tag() == "future_date":
friendlyErrors = append(friendlyErrors,
"Shipping date must be in the future but not more than 30 days ahead")

case e.Field() == "PromoCode" && e.Tag() == "valid_promo":
friendlyErrors = append(friendlyErrors,
"Invalid promo code format. Must be in format PROMO-XXX and be a valid code")

case e.Field() == "Total" && e.Tag() == "check_total":
friendlyErrors = append(friendlyErrors,
"The order total doesn't match the sum of item prices")

default:
friendlyErrors = append(friendlyErrors,
fmt.Sprintf("Validation error on field %s: %s", e.Field(), e.Tag()))
}
}
} else {
friendlyErrors = append(friendlyErrors, "Invalid input format")
}

c.JSON(http.StatusBadRequest, gin.H{"errors": friendlyErrors})
return
}

// Process the validated order
c.JSON(http.StatusOK, gin.H{"message": "Order created successfully"})
}

Advanced: Struct-Level Validation

Sometimes validation needs to look at multiple fields together. Let's implement a struct-level validator:

go
func main() {
router := gin.Default()

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Register struct-level validator
v.RegisterStructValidation(orderValidation, Order{})
}

router.POST("/orders", createOrderHandler)
router.Run(":8080")
}

// Struct-level validation function
func orderValidation(sl validator.StructLevel) {
order := sl.Current().Interface().(Order)

// Validate that order contains at least one item
if len(order.Items) == 0 {
sl.ReportError(order.Items, "Items", "items", "min_items", "")
}

// Validate shipping address for international orders
if strings.HasPrefix(order.CustomerID, "INTL-") && order.ShippingAddress == "" {
sl.ReportError(order.ShippingAddress, "ShippingAddress",
"shipping_address", "required_for_international", "")
}

// Validate that total includes shipping fee for large orders
var itemTotal float64
for _, item := range order.Items {
itemTotal += item.Price * float64(item.Quantity)
}

// Large orders (>$100) should have shipping fee included in total
if itemTotal > 100 && math.Abs(order.Total-itemTotal) < 0.01 {
sl.ReportError(order.Total, "Total", "total", "missing_shipping_fee", "")
}
}

Summary

Custom validators in Gin allow you to implement sophisticated validation logic tailored to your application's business requirements. By extending the built-in validation capabilities, you can ensure that your API processes only valid, well-formatted data, providing a better experience for your users and improving the reliability of your application.

Key points we've covered:

  1. How to register and use custom validators with Gin
  2. Creating validators with parameters for more flexible validation rules
  3. Implementing complex business validation rules
  4. Providing user-friendly error messages
  5. Using struct-level validation to validate multiple fields together

By mastering these techniques, you'll be able to handle even the most complex validation requirements in your Gin applications.

Additional Resources

Exercises

  1. Create a custom validator that validates a credit card number using the Luhn algorithm
  2. Implement a validator that checks if a username is already taken in your database
  3. Build a custom validator that ensures a date range is valid (end date is after start date)
  4. Create a validator for a "strong password" with specific requirements
  5. Implement a validator that uses an external API to validate address information

By completing these exercises, you'll further strengthen your understanding of custom validation in Gin applications and develop practical skills for implementing complex business rules.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)