govalidator
govalidator is a powerful and flexible Go package designed to simplify and enhance data validation for your applications. Built upon the robust github.com/go-playground/validator/v10 library, it extends its capabilities with custom validation rules, particularly for file uploads, and provides convenient utilities for integrating with web frameworks like Fiber.
Features
- Struct Validation: Easily validate Go structs using familiar
validator tags.
- Fiber Integration: Seamlessly validate HTTP request bodies (e.g., JSON, form data) within Fiber applications.
- Custom File Validation: Out-of-the-box support for validating uploaded files (
multipart.FileHeader) with rules like:
is_file: Ensures a field represents a valid, non-empty file.
file_max_size: Validates the maximum allowed file size (e.g., 10MB, 1GB).
file_min_size: Validates the minimum required file size (e.g., 1KB, 500B).
file_ext: Checks if the file has one of the allowed extensions (e.g., jpg;png).
file_mime: Validates the file's MIME type (e.g., image/jpeg;application/pdf).
- Customizable Error Messages: Override default error messages globally or provide specific messages per field and validation tag.
- Field Name Formatting: Transform field names in error messages to more user-friendly formats (e.g.,
camelCase to Camel Case, snake_case to Snake Case).
- Helper Functions: Includes utility functions for string manipulation like
ToCamelCase, ToSnakeCase, and ToHumanReadable.
Installation
go get github.com/openframebox/govalidator
Usage
Basic Struct Validation
Define your struct with validator tags:
package main
import (
"fmt"
"github.com/openframebox/govalidator"
)
type User struct {
Name string `json:"name" validate:"required,min=3,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=130"`
}
func main() {
// Create a new validator instance
v := govalidator.New(nil)
// Valid user
user1 := &User{Name: "John Doe", Email: "[email protected]", Age: 30}
if err := v.ValidateStruct(user1, nil); err != nil {
fmt.Println("Validation Error for user1:", err.Message)
fmt.Println("Details:", err.Details)
} else {
fmt.Println("User1 is valid!")
}
// Invalid user
user2 := &User{Name: "Jo", Email: "invalid-email", Age: 150}
if err := v.ValidateStruct(user2, nil); err != nil {
fmt.Println("\nValidation Error for user2:", err.Message)
fmt.Println("Details:", err.Details)
// Expected output for user2:
// Validation Error for user2: validation failed
// Details: map[Age:field Age must be less than or equal to 130 Email:field Email must be a valid email address Name:field Name must have a minimum length of 3 characters]
} else {
fmt.Println("User2 is valid!")
}
}
Fiber Request Body Validation
govalidator integrates seamlessly with gofiber/fiber/v2. The ValidateFiberRequestBody function automatically handles request body parsing.
package main
import (
"log"
"github.com/gofiber/fiber/v2"
"github.com/openframebox/govalidator"
)
type Product struct {
Name string `json:"name" validate:"required,min=3,max=100"`
Description string `json:"description" validate:"max=500"`
Price float64 `json:"price" validate:"required,gt=0"`
Stock int `json:"stock" validate:"gte=0"`
}
func main() {
app := fiber.New()
v := govalidator.New(nil) // Initialize govalidator
app.Post("/products", func(c *fiber.Ctx) error {
product := new(Product)
// Validate the request body (BodyParser is handled internally)
if err := v.ValidateFiberRequestBody(c, product, nil); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Message,
"details": err.Details,
})
}
// If validation passes, process the product
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"message": "Product created successfully",
"product": product,
})
})
log.Fatal(app.Listen(":3000"))
}
Custom File Validation
govalidator provides custom rules for multipart.FileHeader.
package main
import (
"bytes"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"github.com/gofiber/fiber/v2"
"github.com/openframebox/govalidator"
)
type UploadForm struct {
Image *multipart.FileHeader `form:"image" validate:"required,is_file,file_max_size=5MB,file_ext=jpg;png,file_mime=image/jpeg;image/png"`
Doc *multipart.FileHeader `form:"document" validate:"omitempty,is_file,file_max_size=2MB,file_ext=pdf,file_mime=application/pdf"`
}
func main() {
app := fiber.New()
v := govalidator.New(nil)
app.Post("/upload", func(c *fiber.Ctx) error {
form := new(UploadForm)
// Parse multipart form data
if err := c.BodyParser(form); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Failed to parse form"})
}
// Validate the form struct
if err := v.ValidateStruct(form, nil); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Message,
"details": err.Details,
})
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Files uploaded and validated successfully!"})
})
// Example of how to test this endpoint (e.g., in a test file or separate function)
testUpload(app)
}
func testUpload(app *fiber.App) {
// Create a dummy image file for testing
dummyImageContent := []byte("this is a dummy image content")
imagePath := filepath.Join(os.TempDir(), "test_image.jpg")
os.WriteFile(imagePath, dummyImageContent)
defer os.Remove(imagePath)
// Create a multipart form
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add image file
imageFile, _ := os.Open(imagePath)
defer imageFile.Close()
part, _ := writer.CreateFormFile("image", filepath.Base(imagePath))
part.Write(dummyImageContent)
writer.Close()
req := httptest.NewRequest(http.MethodPost, "/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, _ := app.Test(req)
fmt.Println("\nUpload Test Response Status:", resp.Status)
fmt.Println("Upload Test Response Body:", string(body.Bytes()))
}
Custom Error Messages
You can provide custom error messages for specific fields and validation tags.
package main
import (
"fmt"
"github.com/openframebox/govalidator"
)
type Product struct {
Name string `json:"name" validate:"required,min=3"`
Price float64 `json:"price" validate:"required,gt=0"`
Tags []Tag `json:"tags" validate:"required,dive,required"`
}
type Tag struct {
Name string `json:"name" validate:"required"`
}
func main() {
v := govalidator.New(nil)
customMessages := govalidator.FieldMessages{
"Name": {
"required": ":field is absolutely necessary!", // :field will be replaced by field name
"min": "Product name must be at least :param characters long.", // :param will be replaced by param
},
"Price": {
"gt": "Product price must be greater than zero.",
},
"Tags.*.Name": { // Wildcard for slice elements
"required": "Tag is required",
},
}
product := &Product{Name: "A", Price: -5.0, Tags: []string{"tag1", ""}}
if err := v.ValidateStruct(product, customMessages); err != nil {
fmt.Println("Validation Error:", err.Message)
fmt.Println("Details:", err.Details)
// Expected output:
// Validation Error: validation failed
// Details: map[Name:Product name must be at least 3 characters long. Price:Product price must be greater than zero. Tags[1].Name:Tag is required]
}
}
Providing Messages via Struct
For more encapsulated validation logic, you can implement the govalidator.StructWithFieldMessages interface on your struct. This is useful for keeping validation messages alongside the struct definition.
package main
import (
"fmt"
"github.com/openframebox/govalidator"
)
type User struct {
Username string `json:"username" validate:"required,min=4"`
}
// FieldMessages provides custom messages for the User struct.
func (u *User) FieldMessages() govalidator.FieldMessages {
return govalidator.FieldMessages{
"Username": {
"required": "Username is required.",
"min": "Username must be at least 4 characters long.",
},
}
}
func main() {
v := govalidator.New(nil)
user := &User{Username: "ab"}
if err := v.ValidateStruct(user, nil); err != nil {
fmt.Println("Validation Error:", err.Details)
// Expected: map[Username:Username must be at least 4 characters long.]
}
}
Configure how field names appear in error messages.
package main
import (
"fmt"
"github.com/openframebox/govalidator"
)
type Item struct {
ItemID string `json:"item_id" validate:"required"`
ProductName string `json:"product_name" validate:"min=5"`
}
func main() {
// Example 1: Default (no formatting, uses struct field name)
v1 := govalidator.New(nil)
item1 := &Item{ItemID: "", ProductName: "Short"}
if err := v1.ValidateStruct(item1, nil); err != nil {
fmt.Println("Default Formatting:", err.Details)
// Expected: map[ItemID:field ItemID is required ProductName:field ProductName must have a minimum length of 5 characters]
}
// Example 2: Human-readable formatting
config := &govalidator.Config{
FieldFormatter: govalidator.ToHumanReadable,
}
v2 := govalidator.New(config)
item2 := &Item{ItemID: "", ProductName: "Short"}
if err := v2.ValidateStruct(item2, nil); err != nil {
fmt.Println("Human-Readable Formatting:", err.Details)
// Expected: map[Item ID:field Item ID is required Product Name:field Product Name must have a minimum length of 5 characters]
}
// Example 3: Custom formatter (e.g., to snake_case)
configCustom := &govalidator.Config{
FieldFormatter: govalidator.ToSnakeCase,
}
v3 := govalidator.New(configCustom)
item3 := &Item{ItemID: "", ProductName: "Short"}
if err := v3.ValidateStruct(item3, nil); err != nil {
fmt.Println("Snake_Case Formatting:", err.Details)
// Expected: map[item_id:field item_id is required product_name:field product_name must have a minimum length of 5 characters]
}
}
Helper Functions
govalidator exposes several helper functions for string case conversion:
package main
import (
"fmt"
"github.com/openframebox/govalidator"
)
func main() {
fmt.Println("ToCamelCase('product_name'):", govalidator.ToCamelCase("product_name")) // productName
fmt.Println("ToSnakeCase('ProductName'):", govalidator.ToSnakeCase("ProductName")) // product_name
fmt.Println("ToHumanReadable('productName'):", govalidator.ToHumanReadable("productName")) // product name
fmt.Println("ToHumanReadable('product_name'):", govalidator.ToHumanReadable("product_name")) // product name
}
Custom Validations
You can register your own custom validation functions using RegisterCustomValidation. By default, custom validations are not called if the field is nil or zero-valued. You can change this behavior by passing true as the final argument.
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/openframebox/govalidator"
)
// isEven custom validation checks if an integer is even
func isEven(fl validator.FieldLevel) bool {
num := fl.Field().Int()
return num%2 == 0
}
type MyData struct {
Value int `validate:"required,is_even"`
}
func main() {
v := govalidator.New(nil)
// Register the custom validation
// You can also override its error message if needed
v.RegisterCustomValidation("is_even", isEven)
v.OverrideErrorMessages(map[string]string{
"is_even": "field :field must be an even number",
})
data1 := &MyData{Value: 4}
if err := v.ValidateStruct(data1, nil); err != nil {
fmt.Println("Validation Error for data1:", err.Details)
} else {
fmt.Println("Data1 is valid!") // Expected
}
data2 := &MyData{Value: 5}
if err := v.ValidateStruct(data2, nil); err != nil {
fmt.Println("Validation Error for data2:", err.Details)
// Expected: map[Value:field Value must be an even number]
}
}
Error Handling
ValidateStruct and ValidateFiberRequestBody return a *govalidator.ValidationError if validation fails. Both functions require a pointer to the struct you want to validate. This error type includes:
Message: A general error message (e.g., "validation failed").
Details: A map[string]string where keys are field paths (e.g., Name, Address.Street) and values are the corresponding error messages.
package main
import (
"fmt"
"github.com/openframebox/govalidator"
)
type Address struct {
Street string `json:"street" validate:"required"`
City string `json:"city" validate:"required"`
}
type Person struct {
Name string `json:"name" validate:"required"`
Address Address `json:"address"`
}
func main() {
v := govalidator.New(nil)
person := &Person{
Name: "Alice",
Address: Address{Street: "", City: ""}, // Invalid address
}
if err := v.ValidateStruct(person, nil); err != nil {
fmt.Println("Validation failed:", err.Message)
for field, msg := range err.Details {
fmt.Printf(" Field '%s': %s\n", field, msg)
}
// Expected output:
// Validation failed: validation failed
// Field 'Address.Street': field Street is required
// Field 'Address.City': field City is required
}
}
Configuration
The govalidator.New function accepts an optional *govalidator.Config struct to customize behavior:
type Config struct {
NamespaceFormatter func(field string) string
FieldFormatter func(field string) string
GoStyleNamespaceSelector bool
}
NamespaceFormatter: A function to format the namespace (path) of a field in error details. By default, it removes the root struct name. For example, User.Name becomes Name.
FieldFormatter: A function to format the individual field names within error messages. Defaults to ToHumanReadable (e.g., productName becomes Product Name).
GoStyleNamespaceSelector: A boolean that, when true, changes the namespace selector for slices from [index] to .index (e.g., Tags[0].Name becomes Tags.0.Name). This can be useful for easier parsing.
Overriding Global Error Messages
You can override the default error messages for all validation tags globally.
package main
import (
"fmt"
"github.com/openframebox/govalidator"
)
type MySettings struct {
Port int `validate:"required,gt=1024"`
}
func main() {
v := govalidator.New(nil)
// Override default messages
v.OverrideErrorMessages(map[string]string{
"required": "This field cannot be empty!",
"gt": "Value must be greater than :param.",
})
settings := &MySettings{Port: 80}
if err := v.ValidateStruct(settings, nil); err != nil {
fmt.Println("Validation Error:", err.Details)
// Expected: map[Port:Value must be greater than 1024.]
}
}
Contributing
We welcome contributions! Please see our CONTRIBUTING.md for more details.
License
govalidator is licensed under the MIT License. See the LICENSE file for details.