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:
- Real-World GoLang Project: Car Management System
- Building GUI Applications with Fyne and Go (Golang)
- AWS Cognito Using Golang
- Google Go Programming for Beginners (golang)
- Building a module in Go (Golang)
- Go/Golang Professional Interview Questions
Thanks!