Implement Validation in a Go Web App with Gin
2
min read .
Updated on
Input validation is an important part of any web application. It lets the server reject malformed or incomplete data before business logic runs.
This tutorial builds a small API with Gin and uses go-playground/validator for declarative validation rules.
Project Structure
project/
├── main.go
└── utils/
└── validation.go1. Create the Gin Application
Create main.go with a /register endpoint:
package main
import (
"net/http"
"website/utils"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type User struct {
Name string `json:"name" validate:"required,min=3"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
}
func main() {
r := gin.Default()
r.POST("/register", registerHandler)
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
func registerHandler(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
validate := validator.New()
validationErrors := utils.ValidateStruct(validate, user)
if len(validationErrors) > 0 {
c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": validationErrors})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Registration successful"})
}2. Add Reusable Validation Error Formatting
Create utils/validation.go:
package utils
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
)
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
func ValidateStruct(validate *validator.Validate, data any) []ValidationError {
var errors []ValidationError
if err := validate.Struct(data); err != nil {
validationErrors, ok := err.(validator.ValidationErrors)
if !ok {
return []ValidationError{{Message: "Validation failed"}}
}
for _, fieldError := range validationErrors {
field := strings.ToLower(fieldError.Field())
tag := fieldError.Tag()
var message string
switch tag {
case "required":
message = fmt.Sprintf("Field %s is required", field)
case "min":
message = fmt.Sprintf("Field %s must contain at least %s characters", field, fieldError.Param())
case "email":
message = fmt.Sprintf("Field %s must be a valid email address", field)
default:
message = fmt.Sprintf("Field %s is invalid (%s)", field, tag)
}
errors = append(errors, ValidationError{Field: field, Message: message})
}
}
return errors
}3. Valid Request
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com","password":"secret123"}'Response:
{
"message": "Registration successful"
}4. Invalid Request
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"name":"Al","email":"invalid-email","password":"123"}'Response:
{
"errors": [
{
"field": "name",
"message": "Field name must contain at least 3 characters"
},
{
"field": "email",
"message": "Field email must be a valid email address"
},
{
"field": "password",
"message": "Field password must contain at least 6 characters"
}
]
}Conclusion
Gin and go-playground/validator provide a clean way to keep validation rules close to your request types while returning structured errors to API clients.
As the application grows, consider creating a shared validator instance during startup instead of constructing one for every request, and add rules that reflect your real domain requirements.