Input Validation in Golang APIs Using Go-Validator or Gin Binding

by Didin J. on Aug 06, 2025 Input Validation in Golang APIs Using Go-Validator or Gin Binding

Learn how to validate input in Golang APIs using Gin binding and go-playground/validator.v10 with custom rules, translations, and tests.

Input validation is one of the most critical aspects of building secure and reliable web APIs. Without proper validation, malicious or malformed data can easily slip into your application, leading to potential bugs, security vulnerabilities, or even system crashes.

In the Go ecosystem, input validation is commonly handled using struct tags, with the help of libraries such as go-playground/validator.v10—which powers the built-in validation mechanism in the Gin web framework. Whether you're building a RESTful API or a microservice, validating incoming request data ensures your application behaves as expected and remains safe.

In this tutorial, you will learn how to implement input validation in Golang APIs using:

  • Gin’s built-in binding and validation system

  • The powerful go-playground/validator.v10 library, including custom validators and error handling

By the end of this guide, you'll be able to:

  • Validate incoming JSON payloads using tags

  • Handle validation errors gracefully

  • Write custom validation logic for complex rules

  • Write unit tests to ensure your validations are working correctly

Let’s get started by setting up a simple Gin project!


Project Setup

Before we dive into input validation, let’s set up a basic Golang project using the Gin web framework and the validator.v10 library.

📁 1. Initialize the Go Module

Create a new directory for your project and initialize it as a Go module:

mkdir go-input-validation
cd go-input-validation
go mod init github.com/yourusername/go-input-validation

Replace yourusername with your actual GitHub or module namespace.

📦 2. Install Required Packages

Install Gin and the validator package:

go get -u github.com/gin-gonic/gin
go get -u github.com/go-playground/validator/v10

📂 3. Suggested Project Structure

For simplicity, here’s a minimal project structure:

go-input-validation/
│
├── main.go
├── controllers/
│   └── user_controller.go
├── models/
│   └── user.go
├── validators/
│   └── custom_validators.go
└── go.mod

This structure separates your models, controllers, and custom validations for better scalability.


Creating a Simple REST API with Gin

To demonstrate input validation, we’ll build a basic POST /users endpoint that accepts user details such as name, email, and age.

1. Define the User Struct

Create a file at models/user.go and define the User struct:

package models

type User struct {
	Name  string `json:"name"`
	Email string `json:"email"`
	Age   int    `json:"age"`
}

We’ll enhance this with validation tags in the next section.

2. Create the User Controller

Now, create a file controllers/user_controller.go with a basic handler:

package controllers

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/go-input-validation/models"
)

func CreateUser(c *gin.Context) {
	var user models.User

	if err := c.ShouldBindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// For now, just return the user as a success response
	c.JSON(http.StatusOK, gin.H{
		"message": "User created successfully!",
		"user":    user,
	})
}

3. Set up the Main Function

In main.go, wire everything together:

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/yourusername/go-input-validation/controllers"
)

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

	router.POST("/users", controllers.CreateUser)

	router.Run(":8080")
}

Start your API server:

go run main.go

Now, try a simple POST request with valid JSON:

POST http://localhost:8080/users
Content-Type: application/json

{
  "name": "Alice",
  "email": "[email protected]",
  "age": 25
}

You should get:

{
  "message": "User created successfully!",
  "user": {
    "name": "Alice",
    "email": "[email protected]",
    "age": 25
  }
}


Input Validation Using Gin Binding Tags

Gin provides a powerful and convenient way to validate request payloads using binding tags on struct fields. These tags are powered by the go-playground/validator.v10 library under the hood.

We’ll now add validation rules to our User struct and handle validation errors in the controller.

1. Add Validation Tags to the User Struct

Update models/user.go as follows:

package models

type User struct {
	Name  string `json:"name" binding:"required,min=3,max=50"`
	Email string `json:"email" binding:"required,email"`
	Age   int    `json:"age" binding:"required,gte=18,lte=120"`
}

Explanation of Tags:

  • required: Field must be present and not empty.

  • min, max: Minimum/maximum length for strings.

  • email: Valid email format.

  • gte, lte: Greater than or equal to, less than or equal to for numbers.

2. Update the Controller to Handle Validation Errors

In controllers/user_controller.go, update the error response for clarity:

package controllers

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
	"github.com/yourusername/go-input-validation/models"
)

func CreateUser(c *gin.Context) {
	var user models.User

	if err := c.ShouldBindJSON(&user); err != nil {
		var ve validator.ValidationErrors
		if ok := validator.As(err, &ve); ok {
			out := make(map[string]string)
			for _, fe := range ve {
				out[fe.Field()] = validationErrorMessage(fe)
			}
			c.JSON(http.StatusBadRequest, gin.H{"errors": out})
			return
		}

		// Other errors
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"message": "User created successfully!",
		"user":    user,
	})
}

Add a helper function to map validation errors to friendly messages:

func validationErrorMessage(fe validator.FieldError) string {
	switch fe.Tag() {
	case "required":
		return fe.Field() + " is required"
	case "min":
		return fe.Field() + " must be at least " + fe.Param() + " characters"
	case "max":
		return fe.Field() + " must be at most " + fe.Param() + " characters"
	case "email":
		return "Invalid email address"
	case "gte":
		return fe.Field() + " must be greater than or equal to " + fe.Param()
	case "lte":
		return fe.Field() + " must be less than or equal to " + fe.Param()
	default:
		return fe.Field() + " is invalid"
	}
}

3. Test Validation Errors

Send an invalid request like:

{
  "name": "Al",
  "email": "not-an-email",
  "age": 15
}

You should get:

{
  "errors": {
    "Name": "Name must be at least 3 characters",
    "Email": "Invalid email address",
    "Age": "Age must be greater than or equal to 18"
  }
}

Now your API performs validation using Gin binding tags!


Advanced Validation with go-playground/validator.v10

While Gin’s binding tags cover many basic needs, validator.v10 unlocks powerful features like custom validation functions, nested struct validation, and cross-field logic.

Let’s extend our validation setup to include a custom password rule and explore how to register custom validators in Gin.

1. Extend the User Struct for Password Validation

Update your User struct in models/user.go:

package models

type User struct {
	Name     string `json:"name" binding:"required,min=3,max=50"`
	Email    string `json:"email" binding:"required,email"`
	Age      int    `json:"age" binding:"required,gte=18,lte=120"`
	Password string `json:"password" binding:"required,password"`
}

We’re adding a custom password tag that we’ll define shortly.

2. Create a Custom Validator File

Create validators/custom_validators.go:

package validators

import (
	"regexp"

	"github.com/go-playground/validator/v10"
)

var passwordRegex = regexp.MustCompile(`^[a-zA-Z0-9!@#\$%\^&\*]{8,}$`)

func PasswordValidator(fl validator.FieldLevel) bool {
	password := fl.Field().String()
	return passwordRegex.MatchString(password)
}

This custom validator checks that the password is at least 8 characters and contains only allowed characters.

3. Register the Custom Validator in Gin

Update main.go to register the validator before starting the server:

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
	"github.com/yourusername/go-input-validation/controllers"
	"github.com/yourusername/go-input-validation/validators"
)

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

	// Register custom validator
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("password", validators.PasswordValidator)
	}

	router.POST("/users", controllers.CreateUser)

	router.Run(":8080")
}

Don’t forget to import github.com/gin-gonic/gin/binding.

4. Update Error Message Mapping

In your controllers/user_controller.go, add this case:

case "password":
	return "Password must be at least 8 characters and contain only letters, numbers, or symbols (!@#$%^&*)"

5. Test the Custom Validation

Try this request:

{
  "name": "Jane",
  "email": "[email protected]",
  "age": 30,
  "password": "123"
}

You’ll get:

{
  "errors": {
    "Password": "Password must be at least 8 characters and contain only letters, numbers, or symbols (!@#$%^&*)"
  }
}

Your API now supports advanced validation rules and custom validators using validator.v10.


Localization and Custom Error Messages

User-friendly and localized error messages can significantly improve the developer and end-user experience, especially for frontend validation or public APIs.

validator.v10 supports built-in translation for multiple languages through the universal-translator package.

Let’s see how to add custom error messages in English, and optionally switch to other locales.

1. Install Required Packages

Install the translation libraries:

go get github.com/go-playground/universal-translator
go get github.com/go-playground/locales/en
go get github.com/go-playground/validator/v10/translations/en

2. Initialize the Translator in main.go

Update main.go to register translations:

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/locales/en"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	en_translations "github.com/go-playground/validator/v10/translations/en"
	"github.com/yourusername/go-input-validation/controllers"
	"github.com/yourusername/go-input-validation/validators"
)

var trans ut.Translator

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

	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		// Register custom password validation
		v.RegisterValidation("password", validators.PasswordValidator)

		// Set up English translator
		eng := en.New()
		uni := ut.New(eng, eng)
		trans, _ = uni.GetTranslator("en")

		// Register built-in translations
		en_translations.RegisterDefaultTranslations(v, trans)
	}

	controllers.SetTranslator(trans) // pass it to controller

	router.POST("/users", controllers.CreateUser)

	router.Run(":8080")
}

3. Update Controller to Use Translations

In controllers/user_controller.go, update to use the translator:

package controllers

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
	ut "github.com/go-playground/universal-translator"
	"github.com/yourusername/go-input-validation/models"
)

var trans ut.Translator

func SetTranslator(t ut.Translator) {
	trans = t
}

func CreateUser(c *gin.Context) {
	var user models.User

	if err := c.ShouldBindJSON(&user); err != nil {
		var ve validator.ValidationErrors
		if ok := validator.As(err, &ve); ok {
			out := make(map[string]string)
			for _, fe := range ve {
				out[fe.Field()] = fe.Translate(trans)
			}
			c.JSON(http.StatusBadRequest, gin.H{"errors": out})
			return
		}

		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"message": "User created successfully!",
		"user":    user,
	})
}

4. Custom Translations for Custom Validators

For your custom password validator, add a translation in main.go:

v.RegisterTranslation("password", trans, func(ut ut.Translator) error {
	return ut.Add("password", "{0} must be at least 8 characters and contain only letters, numbers, or symbols", true)
}, func(ut ut.Translator, fe validator.FieldError) string {
	t, _ := ut.T("password", fe.Field())
	return t
})

Make sure to register this after the RegisterDefaultTranslations.

5. Test the Localized Errors

Submit invalid data again:

{
  "name": "",
  "email": "wrong",
  "age": 5,
  "password": "123"
}

You should now see friendly error messages like:

{
  "errors": {
    "Name": "Name is a required field",
    "Email": "Email must be a valid email address",
    "Age": "Age must be greater than or equal to 18",
    "Password": "Password must be at least 8 characters and contain only letters, numbers, or symbols"
  }
}

✅ You've now added localization and custom error messages to your Go API validations.


Best Practices for Input Validation in Go APIs

When your application grows, maintaining clean validation logic becomes essential. Here are some best practices to follow when using Gin and go-playground/validator.v10:

1. Separate Validation from Business Logic

Keep validation concerns out of your controller or business logic as much as possible. For example, place custom validators in their own package (validators/) and handle translation in a centralized place like main.go.

This makes your code more testable and easier to read.

2. Use Struct Tags for Simple Rules

Leverage Gin's binding/validation tags for straightforward constraints like:

  • Required fields

  • Min/max string lengths

  • Email format

  • Numeric ranges

This avoids boilerplate code and lets you quickly define validation constraints.

type LoginRequest struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

3. Use Custom Validators for Complex Rules

When you need to enforce business-specific constraints—like password complexity, matching fields, or custom formats—register your own validators:

v.RegisterValidation("password", validators.PasswordValidator)

4. Handle Nested Structs and Arrays Properly

If your request contains nested structs or arrays, make sure they are validated too:

type Address struct {
	City  string `json:"city" binding:"required"`
	State string `json:"state" binding:"required"`
}

type UserWithAddress struct {
	Name    string  `json:"name" binding:"required"`
	Email   string  `json:"email" binding:"required,email"`
	Address Address `json:"address" binding:"required,dive"`
}

Use the dive keyword to tell the validator to recursively validate elements in slices or nested structs.

5. Validate Before Proceeding with Processing

Make validation your first step in every controller method that accepts user input. This helps fail fast and return helpful messages before proceeding to the database or business logic.

6. Centralize and Reuse Validation Logic

You can even extract validation into a reusable function:

func ValidateInput(input any, c *gin.Context) bool {
	if err := c.ShouldBindJSON(input); err != nil {
		// handle translation and response
		return false
	}
	return true
}

7. Avoid Panic on Validation Failure

Always return structured, friendly error messages instead of panicking or returning raw validator errors. This is especially important in public APIs.

Following these practices helps ensure your Go APIs are secure, user-friendly, and maintainable at scale.


Testing Input Validation

Testing your validation rules is essential to catch regressions and verify that your API responds correctly to both valid and invalid input. In this section, we'll write unit tests using Go’s built-in testing package and the Gin framework’s test helpers.

1. Install Gin’s Test Dependencies

If not already installed, ensure net/http/httptest is available (standard library). No external testing packages are required for basic cases.

2. Create a Test File

Create a file called controllers/user_controller_test.go:

touch controllers/user_controller_test.go

3. Set Up the Test Environment

Here’s the basic setup with sample test cases:

package controllers

import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/yourusername/go-input-validation/models"
	"github.com/yourusername/go-input-validation/validators"
	"github.com/go-playground/locales/en"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	en_translations "github.com/go-playground/validator/v10/translations/en"
)

func setupRouter() *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.Default()

	// Validator setup
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("password", validators.PasswordValidator)

		eng := en.New()
		uni := ut.New(eng, eng)
		trans, _ := uni.GetTranslator("en")
		en_translations.RegisterDefaultTranslations(v, trans)

		v.RegisterTranslation("password", trans, func(ut ut.Translator) error {
			return ut.Add("password", "{0} must be at least 8 characters and contain only letters, numbers, or symbols", true)
		}, func(ut ut.Translator, fe validator.FieldError) string {
			t, _ := ut.T("password", fe.Field())
			return t
		})

		SetTranslator(trans)
	}

	router.POST("/users", CreateUser)
	return router
}

4. Write Test Cases

Add the following tests inside user_controller_test.go:

func TestCreateUser_ValidInput(t *testing.T) {
	router := setupRouter()

	body := `{
		"name": "Alice",
		"email": "[email protected]",
		"age": 25,
		"password": "Secure123!"
	}`

	req, _ := http.NewRequest("POST", "/users", bytes.NewBufferString(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	assert.Contains(t, w.Body.String(), "User created successfully")
}

func TestCreateUser_InvalidInput(t *testing.T) {
	router := setupRouter()

	body := `{
		"name": "Al",
		"email": "bad-email",
		"age": 10,
		"password": "123"
	}`

	req, _ := http.NewRequest("POST", "/users", bytes.NewBufferString(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Contains(t, w.Body.String(), "Name must be at least 3 characters")
	assert.Contains(t, w.Body.String(), "Email must be a valid email address")
	assert.Contains(t, w.Body.String(), "Age must be greater than or equal to 18")
	assert.Contains(t, w.Body.String(), "Password must be at least 8 characters")
}

5. Run the Tests

Run your tests using:

go test ./controllers

You should see output like:

PASS
ok  	github.com/yourusername/go-input-validation/controllers	0.XXXs

With tests in place, your validation logic is now robust and protected against regressions. ✅


Conclusion

Input validation is a foundational part of building secure, stable, and user-friendly APIs. In this tutorial, you’ve learned how to implement both basic and advanced validation techniques in a Golang REST API using the Gin web framework and the go-playground/validator.v10 library.

🔑 You’ve learned how to:

  • Use Gin binding tags to apply built-in validation rules on request structs

  • Handle validation errors gracefully with meaningful messages

  • Create and register custom validators for complex use cases

  • Add localized and user-friendly error messages using universal-translator

  • Write unit tests to ensure validation rules are applied correctly

Whether you're working on small APIs or large-scale microservices, these validation techniques will help you keep your codebase clean, maintainable, and secure.

You can get the full source code on our GitHub.

That is just the basics. If you need more deep learning about the Golang (Go) language and frameworks, you can take the following cheap course:

Thanks!