Validator Plugin for fursy

November 16, 2025 · View on GitHub

Go Reference Go Report Card Coverage

Production-ready validator plugin for fursy HTTP router, integrating go-playground/validator/v10 with automatic conversion to RFC 9457 compliant error responses.

Features

  • Seamless Integration - Implements fursy.Validator interface
  • RFC 9457 Compliance - Automatic conversion to Problem Details format
  • 100+ Validation Tags - Email, URL, UUID, credit card, and more
  • Custom Validators - Register your own validation functions
  • Custom Error Messages - Fully customizable error messages with placeholders
  • Nested Struct Validation - Validates deeply nested structures
  • Zero Config - Works out of the box with sensible defaults
  • Type Safe - Full Go generics support with fursy's Box[Req, Res]
  • 94.3% Test Coverage - Production-ready and battle-tested

Installation

go get github.com/coregx/fursy/plugins/validator

Requirements: Go 1.25+

Quick Start

package main

import (
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/validator"
)

type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Username string `json:"username" validate:"required,min=3,max=50"`
    Age      int    `json:"age" validate:"gte=18,lte=120"`
    Password string `json:"password" validate:"required,min=8"`
}

type UserResponse struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
}

func main() {
    router := fursy.New()

    // Set validator plugin (one line!)
    router.SetValidator(validator.New())

    // Use type-safe handlers - validation is automatic!
    router.POST[CreateUserRequest, UserResponse]("/users", func(c *fursy.Box[CreateUserRequest, UserResponse]) error {
        // c.Bind() automatically validates using struct tags
        if err := c.Bind(); err != nil {
            // Returns RFC 9457 Problem Details with validation errors
            return err
        }

        // ReqBody is validated and type-safe!
        user := createUser(c.ReqBody)
        return c.Created("/users/"+user.ID, user)
    })

    router.Run(":8080")
}

Validation Error Response

When validation fails, the plugin returns RFC 9457 Problem Details:

{
  "type": "about:blank",
  "title": "Validation Failed",
  "status": 422,
  "detail": "4 field(s) failed validation",
  "errors": {
    "email": "Email must be a valid email address",
    "username": "Username must be at least 3 characters long",
    "age": "Age must be greater than or equal to 18",
    "password": "Password must be at least 8 characters long"
  }
}

Common Validation Tags

String Validations

TagDescriptionExample
requiredField must be presentvalidate:"required"
emailMust be valid emailvalidate:"email"
urlMust be valid URLvalidate:"url"
uriMust be valid URIvalidate:"uri"
uuidMust be valid UUIDvalidate:"uuid"
uuid4Must be valid UUID v4validate:"uuid4"
alphaOnly alphabetic charactersvalidate:"alpha"
alphanumOnly alphanumericvalidate:"alphanum"
numericMust be numericvalidate:"numeric"
hexadecimalMust be hexadecimalvalidate:"hexadecimal"
hexcolorMust be hex colorvalidate:"hexcolor"
rgb, rgbaMust be RGB/RGBA colorvalidate:"rgb"
hsl, hslaMust be HSL/HSLA colorvalidate:"hsl"
isbnMust be valid ISBNvalidate:"isbn"
isbn10Must be valid ISBN-10validate:"isbn10"
isbn13Must be valid ISBN-13validate:"isbn13"
jsonMust be valid JSONvalidate:"json"
latitudeMust be valid latitudevalidate:"latitude"
longitudeMust be valid longitudevalidate:"longitude"
ssnMust be valid SSNvalidate:"ssn"
ipv4Must be valid IPv4validate:"ipv4"
ipv6Must be valid IPv6validate:"ipv6"
ipMust be valid IPvalidate:"ip"
macMust be valid MAC addressvalidate:"mac"

String Length/Comparison

TagDescriptionExample
minMinimum lengthvalidate:"min=3"
maxMaximum lengthvalidate:"max=50"
lenExact lengthvalidate:"len=10"
containsMust contain substringvalidate:"contains=@"
containsanyMust contain any ofvalidate:"containsany=abc"
excludesMust not containvalidate:"excludes=admin"
excludesallMust not contain anyvalidate:"excludesall=!@#"
startswithMust start withvalidate:"startswith=https://"
endswithMust end withvalidate:"endswith=.com"

Number Validations

TagDescriptionExample
gtGreater thanvalidate:"gt=0"
gteGreater than or equalvalidate:"gte=18"
ltLess thanvalidate:"lt=100"
lteLess than or equalvalidate:"lte=120"
eqEqual tovalidate:"eq=42"
neNot equal tovalidate:"ne=0"
oneofOne of valuesvalidate:"oneof=red green blue"

Date/Time

TagDescriptionExample
datetimeValid date/time formatvalidate:"datetime=2006-01-02"

Slices/Arrays

All string length tags work on slices for item count:

  • min=2 - At least 2 items
  • max=5 - At most 5 items
  • len=3 - Exactly 3 items

Advanced Usage

Custom Error Messages

v := validator.New(&validator.Options{
    CustomMessages: map[string]string{
        "required": "{field} is required",
        "email":    "Please provide a valid email for {field}",
        "min":      "{field} must have at least {param} characters",
    },
})

router.SetValidator(v)

Placeholders:

  • {field} - Field name
  • {value} - Actual value that failed
  • {param} - Validation parameter (e.g., "3" in min=3)

Custom Validators

v := validator.New()

// Register custom validator
v.RegisterCustomValidator("custom_domain", func(fl validator.FieldLevel) bool {
    domain := fl.Field().String()
    return strings.HasSuffix(domain, "@example.com")
})

router.SetValidator(v)

// Use in struct tags
type SignupRequest struct {
    Email string `validate:"required,custom_domain"`
}

Validation Aliases

Create shorthand tags for complex rules:

v := validator.New()

// Register alias
v.RegisterAlias("password", "required,min=8,max=72,containsany=!@#$%")

router.SetValidator(v)

// Use alias in struct tags
type User struct {
    Password string `validate:"password"`
}

Nested Struct Validation

type Address struct {
    Street  string `validate:"required"`
    City    string `validate:"required"`
    ZipCode string `validate:"required,numeric,len=5"`
}

type User struct {
    Name    string   `validate:"required"`
    Email   string   `validate:"required,email"`
    Address *Address `validate:"required"` // Validates nested struct
}

Using JSON Tag Names in Errors

v := validator.New()

v.RegisterTagNameFunc(func(fld reflect.StructField) string {
    name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
    if name == "-" {
        return ""
    }
    return name
})

router.SetValidator(v)

type User struct {
    Email string `json:"email_address" validate:"required,email"`
}
// Error will use "email_address" instead of "Email"

Validating Single Variables

v := validator.New()

// Validate a single value
email := "user@example.com"
err := v.Var(email, "required,email")
if err != nil {
    // Handle error
}

Integration Patterns

With Middleware

func ValidateMiddleware() fursy.HandlerFunc {
    return func(c *fursy.Context) error {
        // Validation happens automatically in c.Bind()
        // This middleware can add extra checks
        return c.Next()
    }
}

router.Use(ValidateMiddleware())

Manual Validation

router.POST[CreateUserRequest, UserResponse]("/users", func(c *fursy.Box[CreateUserRequest, UserResponse]) error {
    // Option 1: Automatic validation via Bind
    if err := c.Bind(); err != nil {
        return err
    }

    // Option 2: Manual validation
    req := new(CreateUserRequest)
    if err := c.BindJSON(req); err != nil {
        return err
    }

    // Manually validate
    if err := c.Router().Validator().Validate(req); err != nil {
        return err
    }

    // ... process request
})

Production Setup

func setupValidator() *validator.Validator {
    v := validator.New(&validator.Options{
        CustomMessages: map[string]string{
            "required": "{field} is required",
            "email":    "{field} must be a valid email address",
            "min":      "{field} must be at least {param} characters",
            "max":      "{field} must not exceed {param} characters",
        },
    })

    // Register custom validators
    v.RegisterCustomValidator("strong_password", func(fl validator.FieldLevel) bool {
        pass := fl.Field().String()
        // Check for uppercase, lowercase, digit, special char
        return len(pass) >= 8 &&
            regexp.MustCompile(`[A-Z]`).MatchString(pass) &&
            regexp.MustCompile(`[a-z]`).MatchString(pass) &&
            regexp.MustCompile(`[0-9]`).MatchString(pass) &&
            regexp.MustCompile(`[!@#$%^&*]`).MatchString(pass)
    })

    // Register aliases
    v.RegisterAlias("username", "required,min=3,max=50,alphanum")
    v.RegisterAlias("password", "required,strong_password")

    return v
}

func main() {
    router := fursy.New()
    router.SetValidator(setupValidator())
    // ... setup routes
}

Complete Example

See examples/validation/ for a complete working example.

API Reference

Validator

type Validator struct {
    // contains filtered or unexported fields
}

// New creates a new Validator with optional configuration.
func New(opts ...*Options) *Validator

// Validate validates the given data and returns validation errors.
func (v *Validator) Validate(data any) error

// RegisterCustomValidator registers a custom validation function.
func (v *Validator) RegisterCustomValidator(tag string, fn validator.Func) error

// RegisterTagNameFunc registers a function to get custom field names for errors.
func (v *Validator) RegisterTagNameFunc(fn validator.TagNameFunc)

// RegisterAlias registers an alias for validation tags.
func (v *Validator) RegisterAlias(alias, tags string)

// Struct validates a struct and returns validation errors.
func (v *Validator) Struct(data any) error

// Var validates a single variable value against validation tags.
func (v *Validator) Var(field any, tag string) error

Options

type Options struct {
    // TagName is the struct tag name for validation rules.
    // Default: "validate"
    TagName string

    // CustomMessages provides custom error messages for specific tags.
    // Supports placeholders: {field}, {value}, {param}
    CustomMessages map[string]string
}

Performance

  • 94.3% test coverage - Extensively tested
  • Zero allocations - Efficient error conversion
  • Lazy initialization - Validator created only when needed
  • Concurrent safe - Thread-safe validation

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE for details.

Support