Skip to content

Frequently Asked Questions

Find answers to common questions about Souuup. If you don’t see your question here, feel free to open an issue on GitHub.

Souuup is a type-safe validation library for Go that uses generics to provide compile-time type checking for validation rules. It’s designed to be flexible, composable, and easy to use for validating complex data structures.

Why choose Souuup over other validation libraries?

Section titled “Why choose Souuup over other validation libraries?”

Souuup offers several advantages:

  • Type Safety: Uses Go generics for compile-time type checking
  • Composable Design: Build complex validation logic from simple, reusable rules
  • Clear Error Messages: Structured error reporting that matches your data shape
  • Zero Dependencies: Only depends on golang.org/x/exp for constraints
  • Familiar API: Similar to popular validation libraries like Ozzo and Valibot

Souuup requires Go 1.21 or later due to its use of generics and type constraints.

Terminal window
go get github.com/cachesdev/souuup

Then import the packages you need:

import (
"github.com/cachesdev/souuup/u" // Core validation framework
"github.com/cachesdev/souuup/r" // Built-in rules
)

Create a schema that maps field names to u.Field definitions:

type User struct {
Name string
Email string
Age int
}
func ValidateUser(user User) error {
schema := u.Schema{
"name": u.Field(user.Name, r.NotZero, r.MinS(2)),
"email": u.Field(user.Email, r.NotZero, r.ContainsS("@")),
"age": u.Field(user.Age, r.MinN(18)),
}
return u.NewSouuup(schema).Validate()
}

Simply don’t include r.NotZero in the field’s rules. Other rules will only be checked if the field has a non-zero value:

schema := u.Schema{
"name": u.Field(user.Name, r.NotZero), // Required
"phone": u.Field(user.Phone, r.MinS(10)), // Optional, but if provided must be 10+ chars
}

Use nested schemas that mirror your data structure:

schema := u.Schema{
"user": u.Schema{
"name": u.Field(data.User.Name, r.NotZero),
"address": u.Schema{
"street": u.Field(data.User.Address.Street, r.NotZero),
"city": u.Field(data.User.Address.City, r.NotZero),
},
},
}

Use slice-specific rules like r.MinLen, r.Every, and r.Some:

schema := u.Schema{
"interests": u.Field(user.Interests,
r.MinLen[string](1), // At least one interest
r.Every(r.MinS(3)), // Each interest >= 3 characters
),
"scores": u.Field(user.Scores,
r.Every(r.MinN(0)), // All scores >= 0
r.Every(r.MaxN(100)), // All scores <= 100
),
}

Create a function that takes a FieldState[T] and returns an error:

func ValidEmail(fs u.FieldState[string]) error {
email := fs.Value
if !strings.Contains(email, "@") {
return fmt.Errorf("must be a valid email address")
}
return nil
}
// Use it in a field
emailField := u.Field("[email protected]", r.NotZero, ValidEmail)

For parameterized rules, return a rule function:

func MinWords(count int) u.Rule[string] {
return func(fs u.FieldState[string]) error {
words := len(strings.Fields(fs.Value))
if words < count {
return fmt.Errorf("must contain at least %d words", count)
}
return nil
}
}
// Usage
descField := u.Field(description, r.NotZero, MinWords(5))

Validation errors are returned as a structured ValidationError that can be converted to JSON or a map:

err := validator.Validate()
if err != nil {
// As JSON string
fmt.Println(err.Error())
// As structured data
if validationErr, ok := err.(*u.ValidationError); ok {
errorMap := validationErr.ToMap()
// Process errorMap as needed
}
}

Errors are structured to match your data shape:

{
"username": {
"errors": ["length is 2, but needs to be at least 3"]
},
"address": {
"street": {
"errors": ["value is required but has zero value"]
}
}
}

How do I check for errors on specific fields?

Section titled “How do I check for errors on specific fields?”

Use the ToMap() method to access errors programmatically:

if err := validator.Validate(); err != nil {
if validationErr, ok := err.(*u.ValidationError); ok {
errorMap := validationErr.ToMap()
if usernameErrors, exists := errorMap["username"]; exists {
if errors, exists := usernameErrors["errors"]; exists {
fmt.Printf("Username errors: %v", errors)
}
}
}
}

Create rules that check conditions:

func RequiredIf(condition bool) u.Rule[string] {
return func(fs u.FieldState[string]) error {
if condition && fs.Value == "" {
return fmt.Errorf("field is required")
}
return nil
}
}
// Usage
schema := u.Schema{
"country": u.Field(user.Country, r.NotZero),
"state": u.Field(user.State, RequiredIf(user.Country == "US")),
}

How do I validate one field against another?

Section titled “How do I validate one field against another?”

Pass both values to your custom rule:

func PasswordsMatch(password, confirm string) u.Rule[string] {
return func(fs u.FieldState[string]) error {
if password != confirm {
return fmt.Errorf("passwords do not match")
}
return nil
}
}
schema := u.Schema{
"password": u.Field(form.Password, r.NotZero, r.MinS(8)),
"confirm": u.Field(form.Confirm, PasswordsMatch(form.Password, form.Confirm)),
}

How do I validate database constraints (uniqueness, etc.)?

Section titled “How do I validate database constraints (uniqueness, etc.)?”

Create rules that perform database lookups:

type UserService struct {
db *sql.DB
}
func (us *UserService) UniqueEmail(fs u.FieldState[string]) error {
var count int
err := us.db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", fs.Value).Scan(&count)
if err != nil {
return fmt.Errorf("failed to check email uniqueness")
}
if count > 0 {
return fmt.Errorf("email address is already in use")
}
return nil
}
// Usage
userService := &UserService{db: db}
emailField := u.Field(email, r.NotZero, ValidEmail, userService.UniqueEmail)

Create functions that return schemas:

func AddressSchema(addr Address) u.Schema {
return u.Schema{
"street": u.Field(addr.Street, r.NotZero),
"city": u.Field(addr.City, r.NotZero),
"country": u.Field(addr.Country, r.NotZero, r.LenS(2)),
}
}
// Use in multiple places
schema := u.Schema{
"billing_address": AddressSchema(user.BillingAddress),
"shipping_address": AddressSchema(user.ShippingAddress),
}

Yes! Souuup is designed for performance:

  • Zero allocations for most validation operations
  • Compile-time optimizations through generics
  • Early termination on validation failures
  • Minimal overhead compared to reflection-based libraries

Should I create validators once or per request?

Section titled “Should I create validators once or per request?”

For best performance, create validation logic per request but reuse rule functions:

// Reusable rule
var emailRule = func(fs u.FieldState[string]) error {
// validation logic
}
// Create schema per validation (cheap)
func ValidateUser(user User) error {
schema := u.Schema{
"email": u.Field(user.Email, emailRule), // Reuse rule
}
return u.NewSouuup(schema).Validate()
}

Consider these patterns:

// 1. Methods on structs
func (u User) Validate() error {
schema := u.Schema{
"name": u.Field(u.Name, r.NotZero),
}
return u.NewSouuup(schema).Validate()
}
// 2. Separate validation package
package validation
func ValidateUser(user User) error {
// validation logic
}
// 3. Validator struct
type UserValidator struct{}
func (uv UserValidator) Validate(user User) error {
// validation logic
}

What are the best practices for error messages?

Section titled “What are the best practices for error messages?”
  • Be specific: “length is 2, but needs to be at least 3”
  • Be actionable: Tell users what they need to fix
  • Be consistent: Use the same language patterns
  • Be helpful: Provide context when possible

Why am I getting compile-time errors about type mismatches?

Section titled “Why am I getting compile-time errors about type mismatches?”

This is by design! Souuup uses generics to catch type mismatches at compile time:

// This won't compile - string rule on int field
u.Field(42, r.MinS(3)) // ❌
// This is correct
u.Field(42, r.MinN(3)) // ✅

Make sure your rules match your field types.

Why isn’t my validation rule being called?

Section titled “Why isn’t my validation rule being called?”

Check these common issues:

  1. Field is zero and you’re not using r.NotZero:

    // This won't validate an empty string
    u.Field("", CustomRule)
    // This will
    u.Field("", r.NotZero, CustomRule)
  2. Rule has incorrect signature:

    // Wrong
    func BadRule(value string) error { ... }
    // Correct
    func GoodRule(fs u.FieldState[string]) error { ... }
  1. Check each rule individually:

    // Test one rule at a time
    field := u.Field(value, r.MinS(3))
    err := u.NewSouuup(u.Schema{"test": field}).Validate()
  2. Print intermediate values:

    func DebugRule(fs u.FieldState[string]) error {
    fmt.Printf("Validating value: %q\n", fs.Value)
    // your validation logic
    return nil
    }
  3. Use structured error output:

    if err := validator.Validate(); err != nil {
    fmt.Printf("Validation errors: %s\n", err.Error())
    }
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := ValidateUser(user); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]any{
"success": false,
"errors": err.(*u.ValidationError).ToMap(),
})
return
}
// Process valid user...
}
Section titled “How do I use Souuup with popular Go web frameworks?”
func createUser(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := ValidateUser(user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"errors": err.(*u.ValidationError).ToMap(),
})
return
}
// Process user...
c.JSON(http.StatusCreated, gin.H{"success": true})
}

Yes! Validate your protobuf messages after unmarshaling:

func (s *UserService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
// Convert protobuf to internal struct
user := User{
Name: req.Name,
Email: req.Email,
Age: int(req.Age),
}
// Validate
if err := ValidateUser(user); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "validation failed: %v", err)
}
// Process user...
}

We welcome contributions! Here are ways to help:

  1. Report bugs by opening issues on GitHub
  2. Suggest features through GitHub discussions
  3. Submit pull requests for bug fixes or new features
  4. Improve documentation by submitting corrections or additions
  5. Share examples of how you’re using Souuup

Please include:

  • Go version and Souuup version
  • Minimal code example that reproduces the issue
  • Expected vs actual behavior
  • Error messages (if any)

Open an issue with:

  • Description of the validation need
  • Example use cases
  • Proposed API design
  • Whether it’s domain-specific or generally useful

Still have questions? Open an issue on GitHub or check out our examples for more practical guidance!