Validation
Validate HTTP requests and form data with Velocity's declarative rule-based validation system.
Velocity provides a flexible, extensible validation system for validating HTTP requests, form data, and general data structures. The validation package uses a declarative, rule-based approach similar to Laravel but designed for Go’s type system.
Quick Start
import "github.com/velocitykode/velocity/pkg/validation"
func validateUser(data map[string]interface{}) error {
rules := validation.Rules{
"email": "required|email",
"password": "required|min:8",
"age": "required|numeric|min:18",
"username": "required|alpha_num",
}
validated, err := validation.Validate(data, rules)
if err != nil {
// Handle validation errors
validationErr := err.(validation.ValidationErrors)
fmt.Println("Validation failed:", validationErr.Error())
return err
}
// Use validated data
email := validated.GetString("email")
age := validated.GetInt("age")
return nil
}import (
"github.com/velocitykode/velocity/pkg/http"
"github.com/velocitykode/velocity/pkg/validation"
)
func RegisterUser(c *http.Context) error {
// Validate HTTP request
rules := validation.Rules{
"name": "required|string",
"email": "required|email",
"password": "required|min:8|confirmed",
"age": "required|numeric|min:18",
"terms": "required|accepted",
}
validated, err := validation.ValidateRequest(c.Request, rules)
if err != nil {
return c.JSON(422, map[string]interface{}{
"errors": err.(validation.ValidationErrors).All(),
})
}
// Create user with validated data
user := &User{
Name: validated.GetString("name"),
Email: validated.GetString("email"),
Password: validated.GetString("password"),
Age: validated.GetInt("age"),
}
return c.JSON(200, user)
}import "github.com/velocitykode/velocity/pkg/validation"
func validateWithCustomMessages(data map[string]interface{}) error {
rules := validation.Rules{
"email": "required|email",
"password": "required|min:8",
}
// Set custom error messages
validator := validation.Get()
validator.SetMessages(validation.Messages{
"email.required": "Please provide your email address",
"email.email": "Please enter a valid email address",
"password.required": "Password is required",
"password.min": "Password must be at least 8 characters",
})
validated, err := validator.Validate(data, rules)
if err != nil {
return err
}
return nil
}Configuration
The validation package works out of the box with no configuration required. However, you can customize behavior through environment variables:
# Stop validation on first error (default: false)
VALIDATION_STOP_ON_FIRST=false
# Default locale for error messages
VALIDATION_DEFAULT_LOCALE=en
# Bail on first rule failure per field (default: true)
VALIDATION_BAIL_ON_ERROR=trueAvailable Rules
Presence Rules
required
Field must be present and not empty:
rules := validation.Rules{
"name": "required",
}
// ✓ "John"
// ✗ "" (empty string)
// ✗ nilnullable
Field can be null or empty:
rules := validation.Rules{
"middle_name": "nullable|string",
}
// ✓ "Smith"
// ✓ ""
// ✓ nilfilled
If field is present, it must not be empty:
rules := validation.Rules{
"bio": "filled",
}
// ✓ "Hello world"
// ✓ (field not present)
// ✗ "" (empty when present)present
Field must be present (but can be empty):
rules := validation.Rules{
"accept_terms": "present",
}
// ✓ "" (present but empty)
// ✓ "yes"
// ✗ (field not in data)Type Rules
string
Value must be a string:
rules := validation.Rules{
"name": "string",
}
// ✓ "John"
// ✗ 123integer
Value must be an integer:
rules := validation.Rules{
"age": "integer",
}
// ✓ 25
// ✗ "25"
// ✗ 25.5numeric
Value must be numeric (integer or float):
rules := validation.Rules{
"price": "numeric",
}
// ✓ 99
// ✓ 99.99
// ✗ "99.99"boolean
Value must be boolean:
rules := validation.Rules{
"active": "boolean",
}
// ✓ true
// ✓ false
// ✗ "true"array
Value must be an array:
rules := validation.Rules{
"tags": "array",
}
// ✓ []string{"go", "web"}
// ✗ "go,web"String Rules
Value must be a valid email address:
rules := validation.Rules{
"email": "email",
}
// ✓ "user@example.com"
// ✗ "invalid-email"url
Value must be a valid URL:
rules := validation.Rules{
"website": "url",
}
// ✓ "https://example.com"
// ✗ "not-a-url"alpha
Value must contain only alphabetic characters:
rules := validation.Rules{
"name": "alpha",
}
// ✓ "John"
// ✗ "John123"alpha_num
Value must contain only alphanumeric characters:
rules := validation.Rules{
"username": "alpha_num",
}
// ✓ "user123"
// ✗ "user_123"alpha_dash
Value must contain only alphanumeric characters, dashes, and underscores:
rules := validation.Rules{
"slug": "alpha_dash",
}
// ✓ "my-post_title"
// ✗ "my post!"Size Rules
min
Minimum value or length:
rules := validation.Rules{
"password": "min:8", // String: min 8 characters
"age": "numeric|min:18", // Number: min value 18
"tags": "array|min:2", // Array: min 2 elements
}max
Maximum value or length:
rules := validation.Rules{
"bio": "max:500", // String: max 500 characters
"age": "numeric|max:120", // Number: max value 120
"tags": "array|max:10", // Array: max 10 elements
}size
Exact value or length:
rules := validation.Rules{
"zip_code": "size:5", // String: exactly 5 characters
"rating": "numeric|size:5", // Number: exactly 5
"choices": "array|size:3", // Array: exactly 3 elements
}between
Value must be between two values:
rules := validation.Rules{
"age": "numeric|between:18,65",
"username": "between:3,20",
}Comparison Rules
same
Field must have the same value as another field:
rules := validation.Rules{
"password": "required",
"password_confirm": "same:password",
}different
Field must have a different value from another field:
rules := validation.Rules{
"email": "required|email",
"alt_email": "different:email",
}confirmed
Field must have a matching confirmation field:
rules := validation.Rules{
"password": "confirmed", // Looks for "password_confirmation"
}
// Form data must include both:
// password: "secret123"
// password_confirmation: "secret123"in
Value must be in a list of values:
rules := validation.Rules{
"role": "in:admin,user,moderator",
}
// ✓ "admin"
// ✗ "superadmin"not_in
Value must not be in a list of values:
rules := validation.Rules{
"username": "not_in:admin,root,system",
}
// ✓ "john"
// ✗ "admin"accepted
Field must be yes, on, 1, or true (useful for terms of service):
rules := validation.Rules{
"terms": "accepted",
}
// ✓ "yes", "on", "1", true
// ✗ "no", "off", "0", falseAPI Reference
Global Functions
Validate
Validate data against rules:
data := map[string]interface{}{
"email": "user@example.com",
"age": 25,
}
rules := validation.Rules{
"email": "required|email",
"age": "required|numeric|min:18",
}
validated, err := validation.Validate(data, rules)
if err != nil {
// Handle validation errors
validationErr := err.(validation.ValidationErrors)
for field, messages := range validationErr.All() {
fmt.Printf("%s: %v\n", field, messages)
}
return err
}
// Access validated data
email := validated.GetString("email")
age := validated.GetInt("age")ValidateRequest
Validate HTTP request:
func handler(c *http.Context) error {
rules := validation.Rules{
"email": "required|email",
"name": "required|string",
}
validated, err := validation.ValidateRequest(c.Request, rules)
if err != nil {
return c.JSON(422, map[string]interface{}{
"errors": err.(validation.ValidationErrors).All(),
})
}
return c.JSON(200, validated.All())
}ValidateValue
Validate a single value:
email := "user@example.com"
err := validation.ValidateValue(email, "required|email")
if err != nil {
fmt.Println("Invalid email:", err)
}ValidatedData Methods
Get
Get any value:
value := validated.Get("field_name")GetString
Get a string value:
name := validated.GetString("name")GetInt
Get an integer value:
age := validated.GetInt("age")GetBool
Get a boolean value:
active := validated.GetBool("active")All
Get all validated data:
data := validated.All()HasErrors
Check if validation failed:
if validated.HasErrors() {
// Handle errors
}Errors
Get validation errors:
errors := validated.Errors()ValidationErrors Methods
Error
Get error message as string:
err := validation.Validate(data, rules)
if err != nil {
fmt.Println(err.Error())
// Output: "validation failed: email: email is required; age: age must be numeric"
}HasError
Check if a field has errors:
if errors.HasError("email") {
// Email field has validation errors
}First
Get the first error for a field:
emailError := errors.First("email")
fmt.Println(emailError)All
Get all errors:
allErrors := errors.All()
for field, messages := range allErrors {
fmt.Printf("%s: %v\n", field, messages)
}Count
Get total error count:
count := errors.Count()
fmt.Printf("Total validation errors: %d\n", count)IsEmpty
Check if there are no errors:
if errors.IsEmpty() {
// No validation errors
}Custom Validation Rules
You can register custom validation rules:
import "github.com/velocitykode/velocity/pkg/validation"
func init() {
// Register a custom "strong_password" rule
validation.RegisterRule("strong_password", func(
field string,
value interface{},
params []string,
data map[string]interface{},
) error {
password, ok := value.(string)
if !ok {
return fmt.Errorf("%s must be a string", field)
}
// Check password strength
hasUpper := false
hasLower := false
hasNumber := false
hasSpecial := false
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
return fmt.Errorf(
"%s must contain uppercase, lowercase, number, and special character",
field,
)
}
return nil
})
}
// Usage
rules := validation.Rules{
"password": "required|min:8|strong_password",
}Custom Error Messages
Per-Validator Messages
validator := validation.Get()
validator.SetMessages(validation.Messages{
"email.required": "We need your email address",
"email.email": "That doesn't look like a valid email",
"password.min": "Password should be at least 8 characters",
})Inline Messages
func validateWithMessages(data map[string]interface{}) error {
rules := validation.Rules{
"email": "required|email",
"password": "required|min:8",
}
messages := validation.Messages{
"email.required": "Email is required",
"email.email": "Invalid email format",
"password.required": "Password is required",
"password.min": "Password must be at least 8 characters",
}
validator := validation.Get()
validator.SetMessages(messages)
return validator.Validate(data, rules)
}Best Practices
1. Validate Early
Validate input as soon as it enters your application:
func CreateUser(c *http.Context) error {
// Validate first, before any business logic
validated, err := validation.ValidateRequest(c.Request, validation.Rules{
"email": "required|email",
"name": "required|string",
})
if err != nil {
return c.JSON(422, map[string]interface{}{
"errors": err.(validation.ValidationErrors).All(),
})
}
// Now proceed with business logic
user := createUserFromValidatedData(validated)
return c.JSON(201, user)
}2. Use Type-Safe Getters
Use the typed getters to avoid type assertions:
// Good: Type-safe
age := validated.GetInt("age")
email := validated.GetString("email")
active := validated.GetBool("active")
// Avoid: Manual type assertion
age := validated.Get("age").(int) // Can panic!3. Provide Clear Error Messages
Use custom messages to improve user experience:
messages := validation.Messages{
"email.email": "Please enter a valid email address",
"password.min": "Your password needs to be at least 8 characters",
"terms.accepted": "You must accept the terms of service",
}4. Combine Rules Appropriately
Order rules from general to specific:
rules := validation.Rules{
// Good: required first, then type, then constraints
"age": "required|numeric|min:18|max:120",
// Less clear
"age": "min:18|max:120|required|numeric",
}5. Create Reusable Rule Sets
var (
EmailRules = "required|email"
PasswordRules = "required|min:8|strong_password"
PhoneRules = "required|numeric|size:10"
)
rules := validation.Rules{
"email": EmailRules,
"password": PasswordRules,
"phone": PhoneRules,
}Complete Example
Here’s a complete example of a user registration endpoint with validation:
package handlers
import (
"github.com/velocitykode/velocity/pkg/http"
"github.com/velocitykode/velocity/pkg/validation"
)
type UserHandler struct{}
func (uc *UserHandler) Register(c *http.Context) error {
// Define validation rules
rules := validation.Rules{
"name": "required|string|min:2|max:100",
"email": "required|email",
"password": "required|min:8|confirmed",
"password_confirmation": "required",
"age": "required|numeric|min:18",
"terms": "required|accepted",
"newsletter": "nullable|boolean",
}
// Custom error messages
messages := validation.Messages{
"name.required": "Please tell us your name",
"name.min": "Name must be at least 2 characters",
"email.required": "We need your email address",
"email.email": "Please enter a valid email address",
"password.required": "Password is required",
"password.min": "Password must be at least 8 characters",
"password.confirmed": "Passwords do not match",
"age.required": "Please provide your age",
"age.min": "You must be at least 18 years old",
"terms.accepted": "You must accept the terms of service",
}
// Set custom messages
validator := validation.Get()
validator.SetMessages(messages)
// Validate request
validated, err := validator.ValidateRequest(c.Request, rules)
if err != nil {
// Return validation errors
validationErr := err.(validation.ValidationErrors)
return c.JSON(422, map[string]interface{}{
"message": "Validation failed",
"errors": validationErr.All(),
})
}
// Create user from validated data
user := &User{
Name: validated.GetString("name"),
Email: validated.GetString("email"),
Password: hashPassword(validated.GetString("password")),
Age: validated.GetInt("age"),
NewsletterOptIn: validated.GetBool("newsletter"),
}
// Save user to database
if err := user.Save(); err != nil {
return c.JSON(500, map[string]string{
"error": "Failed to create user",
})
}
return c.JSON(201, map[string]interface{}{
"message": "Registration successful",
"user": user,
})
}
func (uc *UserHandler) UpdateProfile(c *http.Context) error {
userID := c.Param("id")
rules := validation.Rules{
"name": "nullable|string|min:2|max:100",
"email": "nullable|email",
"bio": "nullable|string|max:500",
"age": "nullable|numeric|min:18",
}
validated, err := validation.ValidateRequest(c.Request, rules)
if err != nil {
return c.JSON(422, map[string]interface{}{
"errors": err.(validation.ValidationErrors).All(),
})
}
// Update user with validated data
user := FindUserByID(userID)
if user == nil {
return c.JSON(404, map[string]string{
"error": "User not found",
})
}
// Only update fields that were provided
for field, value := range validated.All() {
switch field {
case "name":
user.Name = value.(string)
case "email":
user.Email = value.(string)
case "bio":
user.Bio = value.(string)
case "age":
user.Age = validated.GetInt("age")
}
}
user.Save()
return c.JSON(200, map[string]interface{}{
"message": "Profile updated successfully",
"user": user,
})
}Testing
func TestValidation(t *testing.T) {
data := map[string]interface{}{
"email": "user@example.com",
"password": "secret123",
"age": 25,
}
rules := validation.Rules{
"email": "required|email",
"password": "required|min:8",
"age": "required|numeric|min:18",
}
validated, err := validation.Validate(data, rules)
assert.NoError(t, err)
assert.Equal(t, "user@example.com", validated.GetString("email"))
assert.Equal(t, 25, validated.GetInt("age"))
}
func TestValidationErrors(t *testing.T) {
data := map[string]interface{}{
"email": "invalid-email",
"age": "not-a-number",
}
rules := validation.Rules{
"email": "required|email",
"age": "required|numeric",
}
_, err := validation.Validate(data, rules)
assert.Error(t, err)
validationErr := err.(validation.ValidationErrors)
assert.True(t, validationErr.HasError("email"))
assert.True(t, validationErr.HasError("age"))
assert.Equal(t, 2, validationErr.Count())
}
