Examples
Real-world validation patterns and integration examples
7 minute read
Complete examples showing how to use the validation package in real-world scenarios.
Basic REST API
Create User Endpoint
package main
import (
"context"
"encoding/json"
"net/http"
"rivaas.dev/validation"
)
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=20,alphanum"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=18,max=120"`
Password string `json:"password" validate:"required,min=8"`
}
var validator = validation.MustNew(
validation.WithRedactor(func(path string) bool {
return strings.Contains(path, "password")
}),
validation.WithMaxErrors(10),
)
func CreateUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if err := validator.Validate(ctx, &req); err != nil {
handleValidationError(w, err)
return
}
// Create user
user, err := createUser(ctx, req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func handleValidationError(w http.ResponseWriter, err error) {
var verr *validation.Error
if !errors.As(err, &verr) {
http.Error(w, "validation failed", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(map[string]any{
"error": "validation_failed",
"fields": verr.Fields,
})
}
Update User Endpoint (PATCH)
type UpdateUserRequest struct {
Email *string `json:"email" validate:"omitempty,email"`
Age *int `json:"age" validate:"omitempty,min=18,max=120"`
}
func UpdateUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Read raw body
rawJSON, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
// Compute which fields are present
presence, err := validation.ComputePresence(rawJSON)
if err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
// Parse into struct
var req UpdateUserRequest
if err := json.Unmarshal(rawJSON, &req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
// Validate only present fields
if err := validation.ValidatePartial(ctx, &req, presence); err != nil {
handleValidationError(w, err)
return
}
// Update user
userID := r.PathValue("id")
if err := updateUser(ctx, userID, req, presence); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
Custom Validation Methods
Order Validation
type CreateOrderRequest struct {
UserID int `json:"user_id"`
Items []OrderItem `json:"items"`
CouponCode string `json:"coupon_code"`
Total float64 `json:"total"`
}
type OrderItem struct {
ProductID int `json:"product_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
Price float64 `json:"price" validate:"required,min=0"`
}
func (r *CreateOrderRequest) ValidateContext(ctx context.Context) error {
var verr validation.Error
// Validate user exists
if !userExists(ctx, r.UserID) {
verr.Add("user_id", "not_found", "user does not exist", nil)
}
// Validate items
if len(r.Items) == 0 {
verr.Add("items", "required", "at least one item required", nil)
}
var calculatedTotal float64
for i, item := range r.Items {
// Validate product and price
product, err := getProduct(ctx, item.ProductID)
if err != nil {
verr.Add(
fmt.Sprintf("items.%d.product_id", i),
"not_found",
"product does not exist",
nil,
)
continue
}
if item.Price != product.Price {
verr.Add(
fmt.Sprintf("items.%d.price", i),
"mismatch",
"price does not match current product price",
map[string]any{
"expected": product.Price,
"actual": item.Price,
},
)
}
calculatedTotal += item.Price * float64(item.Quantity)
}
// Validate coupon
if r.CouponCode != "" {
discount, err := validateCoupon(ctx, r.CouponCode)
if err != nil {
verr.Add("coupon_code", "invalid", err.Error(), nil)
} else {
calculatedTotal -= discount
}
}
// Validate total
if math.Abs(r.Total-calculatedTotal) > 0.01 {
verr.Add(
"total",
"mismatch",
"total does not match calculated amount",
map[string]any{
"expected": calculatedTotal,
"actual": r.Total,
},
)
}
if verr.HasErrors() {
return &verr
}
return nil
}
Custom Validation Tags
Application Validator
package app
import (
"regexp"
"unicode"
"github.com/go-playground/validator/v10"
"rivaas.dev/validation"
)
var (
phoneRegex = regexp.MustCompile(`^\+?[1-9]\d{1,14}$`)
usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{3,20}$`)
)
var Validator = validation.MustNew(
// Custom tags
validation.WithCustomTag("phone", validatePhone),
validation.WithCustomTag("username", validateUsername),
validation.WithCustomTag("strong_password", validateStrongPassword),
// Security
validation.WithRedactor(sensitiveFieldRedactor),
validation.WithMaxErrors(20),
// Custom messages
validation.WithMessages(map[string]string{
"required": "is required",
"email": "must be a valid email address",
}),
)
func validatePhone(fl validator.FieldLevel) bool {
return phoneRegex.MatchString(fl.Field().String())
}
func validateUsername(fl validator.FieldLevel) bool {
return usernameRegex.MatchString(fl.Field().String())
}
func validateStrongPassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
if len(password) < 8 {
return false
}
var hasUpper, hasLower, hasDigit, hasSpecial bool
for _, c := range password {
switch {
case unicode.IsUpper(c):
hasUpper = true
case unicode.IsLower(c):
hasLower = true
case unicode.IsDigit(c):
hasDigit = true
case unicode.IsPunct(c) || unicode.IsSymbol(c):
hasSpecial = true
}
}
return hasUpper && hasLower && hasDigit && hasSpecial
}
func sensitiveFieldRedactor(path string) bool {
pathLower := strings.ToLower(path)
return strings.Contains(pathLower, "password") ||
strings.Contains(pathLower, "token") ||
strings.Contains(pathLower, "secret") ||
strings.Contains(pathLower, "card") ||
strings.Contains(pathLower, "cvv")
}
Using Custom Tags
type Registration struct {
Username string `validate:"required,username"`
Phone string `validate:"required,phone"`
Password string `validate:"required,strong_password"`
}
func Register(w http.ResponseWriter, r *http.Request) {
var req Registration
json.NewDecoder(r.Body).Decode(&req)
if err := app.Validator.Validate(r.Context(), &req); err != nil {
handleValidationError(w, err)
return
}
// Process registration
}
JSON Schema Validation
type Product struct {
Name string `json:"name"`
Price float64 `json:"price"`
Category string `json:"category"`
InStock bool `json:"in_stock"`
Tags []string `json:"tags"`
}
func (p Product) JSONSchema() (id, schema string) {
return "product-v1", `{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"price": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "books", "food"]
},
"in_stock": {
"type": "boolean"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
}
},
"required": ["name", "price", "category"],
"additionalProperties": false
}`
}
func CreateProduct(w http.ResponseWriter, r *http.Request) {
var product Product
json.NewDecoder(r.Body).Decode(&product)
// Validates using JSON Schema
if err := validation.Validate(r.Context(), &product); err != nil {
handleValidationError(w, err)
return
}
// Save product
}
Multi-Strategy Validation
type User struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=20"`
}
// Add JSON Schema
func (u User) JSONSchema() (id, schema string) {
return "user-v1", `{
"type": "object",
"properties": {
"email": {"type": "string", "format": "email"},
"username": {"type": "string", "minLength": 3, "maxLength": 20}
}
}`
}
// Add custom validation
func (u *User) ValidateContext(ctx context.Context) error {
// Check username uniqueness
if usernameExists(ctx, u.Username) {
return errors.New("username already taken")
}
return nil
}
// Run all strategies
err := validation.Validate(ctx, &user,
validation.WithRunAll(true),
)
Testing
Testing Validation
func TestUserValidation(t *testing.T) {
tests := []struct {
name string
user CreateUserRequest
wantErr bool
errCode string
}{
{
name: "valid user",
user: CreateUserRequest{
Username: "alice",
Email: "alice@example.com",
Age: 25,
Password: "SecurePass123!",
},
wantErr: false,
},
{
name: "invalid email",
user: CreateUserRequest{
Username: "alice",
Email: "invalid",
Age: 25,
Password: "SecurePass123!",
},
wantErr: true,
errCode: "tag.email",
},
{
name: "underage",
user: CreateUserRequest{
Username: "alice",
Email: "alice@example.com",
Age: 15,
Password: "SecurePass123!",
},
wantErr: true,
errCode: "tag.min",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.Validate(context.Background(), &tt.user)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
return
}
var verr *validation.Error
if !errors.As(err, &verr) {
t.Error("expected validation.Error")
return
}
if tt.errCode != "" && !verr.HasCode(tt.errCode) {
t.Errorf("expected error code %s", tt.errCode)
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}
Testing Partial Validation
func TestPartialValidation(t *testing.T) {
tests := []struct {
name string
json string
wantErr bool
}{
{
name: "valid email update",
json: `{"email": "new@example.com"}`,
wantErr: false,
},
{
name: "invalid email update",
json: `{"email": "invalid"}`,
wantErr: true,
},
{
name: "valid age update",
json: `{"age": 25}`,
wantErr: false,
},
{
name: "underage update",
json: `{"age": 15}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
presence, _ := validation.ComputePresence([]byte(tt.json))
var req UpdateUserRequest
json.Unmarshal([]byte(tt.json), &req)
err := validation.ValidatePartial(context.Background(), &req, presence)
if (err != nil) != tt.wantErr {
t.Errorf("ValidatePartial() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Integration with rivaas/router
import "rivaas.dev/router"
func Handler(c *router.Context) error {
var req CreateUserRequest
if err := c.BindJSON(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid JSON",
})
}
if err := validator.Validate(c.Request().Context(), &req); err != nil {
var verr *validation.Error
if errors.As(err, &verr) {
return c.JSON(http.StatusUnprocessableEntity, map[string]any{
"error": "validation_failed",
"fields": verr.Fields,
})
}
return err
}
// Process request
return c.JSON(http.StatusOK, createUser(req))
}
Integration with rivaas/app
import "rivaas.dev/app"
func Handler(c *app.Context) error {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
return err // Automatically handled
}
// Validation happens automatically with app.Context
// But you can also validate manually:
if err := validator.Validate(c.Context(), &req); err != nil {
return err // Automatically converted to proper HTTP response
}
return c.JSON(http.StatusOK, createUser(req))
}
Performance Tips
Reuse Validator Instances
// Good - create once
var appValidator = validation.MustNew(
validation.WithMaxErrors(10),
)
func Handler1(ctx context.Context, req Request1) error {
return appValidator.Validate(ctx, &req)
}
func Handler2(ctx context.Context, req Request2) error {
return appValidator.Validate(ctx, &req)
}
// Bad - create every time (slow)
func Handler(ctx context.Context, req Request) error {
validator := validation.MustNew()
return validator.Validate(ctx, &req)
}
Use Package-Level Functions for Simple Cases
// Simple validation - use package-level function
err := validation.Validate(ctx, &req)
// Complex validation - create validator instance
validator := validation.MustNew(
validation.WithCustomTag("phone", validatePhone),
validation.WithRedactor(redactor),
)
err := validator.Validate(ctx, &req)
Next Steps
- Installation - Set up the validation package
- Basic Usage - Learn validation fundamentals
- API Reference - Complete API documentation
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.