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 the Binding interface with Bind(*http.Request, any) error for each content type (JSON, XML, Form, etc.)
  • binding/binding.go (lines 51-68): Declares the StructValidator interface requiring ValidateStruct(any) error and Engine() any
  • binding/default_validator.go (lines 43-70): Implements ValidateStruct with support for structs, pointers, and slices/arrays, including aggregation of slice element errors
  • binding/default_validator.go (lines 81-88): Exposes the underlying *validator.Validate engine for custom rule registration
  • context.go: Provides *gin.Context methods (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 Validator variable that implements the StructValidator interface 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.Validate to access RegisterValidation for field-level rules or RegisterStructValidation for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →