Apps Artificial Intelligence CSS DevOps Go JavaScript Laravel Linux MongoDB MySQL PHP Python Rust Svelte Vue

Implement Validation in a Go Web App with Gin

2 min read .
Implement Validation in a Go Web App with Gin

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.go

1. 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.

Related Posts

chevron-up