Mirror Your Data Structure
Keep your validation schema structure as close as possible to your data structure for clarity.
// Good: Mirrors the struct"user": u.Schema{ "profile": u.Schema{ "name": u.Field(...) }}
Souuup excels at validating complex, nested data structures. This guide shows you how to create hierarchical validation schemas that mirror your data structures.
Nested schemas allow you to validate complex objects with multiple levels of data. Each level can have its own validation rules and error reporting.
// Simple nested structureuserSchema := u.Schema{ "profile": u.Schema{ "name": u.Field("John Doe", r.MinS(2)), "age": u.Field(25, r.MinN(18)), }, "settings": u.Schema{ "theme": u.Field("dark", r.InS([]string{"light", "dark"})), "notifications": u.Field(true, r.NotZero), },}
Let’s start with a simple user profile example:
type Address struct { Street string City string Country string PostCode string}
type User struct { Name string Email string Address Address}
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("@")), "address": u.Schema{ "street": u.Field(user.Address.Street, r.NotZero, r.MinS(5)), "city": u.Field(user.Address.City, r.NotZero, r.MinS(2)), "country": u.Field(user.Address.Country, r.NotZero, r.LenS(2)), "post_code": u.Field(user.Address.PostCode, r.NotZero), }, }
s := u.NewSouuup(schema) return s.Validate()}
Souuup supports arbitrarily deep nesting for complex data structures:
type Company struct { Name string Address Address Contact ContactInfo}
type ContactInfo struct { Phone string Email string Website string Social SocialMedia}
type SocialMedia struct { Twitter string LinkedIn string GitHub string}
func ValidateCompany(company Company) error { schema := u.Schema{ "name": u.Field(company.Name, r.NotZero, r.MinS(2)), "address": u.Schema{ "street": u.Field(company.Address.Street, r.NotZero), "city": u.Field(company.Address.City, r.NotZero), "country": u.Field(company.Address.Country, r.NotZero), }, "contact": u.Schema{ "phone": u.Field(company.Contact.Phone, r.NotZero), "email": u.Field(company.Contact.Email, r.NotZero, r.ContainsS("@")), "website": u.Field(company.Contact.Website, r.ContainsS("http")), "social": u.Schema{ "twitter": u.Field(company.Contact.Social.Twitter, r.MinS(0)), "linkedin": u.Field(company.Contact.Social.LinkedIn, r.MinS(0)), "github": u.Field(company.Contact.Social.GitHub, r.MinS(0)), }, }, }
return u.NewSouuup(schema).Validate()}
Validating arrays or slices of complex objects requires combining slice rules with nested schemas:
type TeamMember struct { Name string Role string Skills []string Active bool}
type Team struct { Name string Members []TeamMember}
func ValidateTeam(team Team) error { schema := u.Schema{ "name": u.Field(team.Name, r.NotZero, r.MinS(2)), "members": u.Field(team.Members, r.MinLen[TeamMember](1), // At least one member r.MaxLen[TeamMember](20), // At most 20 members // Validate each member individually r.Every(func(fs u.FieldState[TeamMember]) error { member := fs.Value memberSchema := u.Schema{ "name": u.Field(member.Name, r.NotZero, r.MinS(2)), "role": u.Field(member.Role, r.NotZero), "skills": u.Field(member.Skills, r.MinLen[string](1)), "active": u.Field(member.Active, r.NotZero), } return u.NewSouuup(memberSchema).Validate() }), ), }
return u.NewSouuup(schema).Validate()}
type Course struct { Name string Modules []Module}
type Module struct { Title string Lessons []Lesson}
type Lesson struct { Title string Duration int // minutes Content string}
func ValidateCourse(course Course) error { schema := u.Schema{ "name": u.Field(course.Name, r.NotZero, r.MinS(3)), "modules": u.Field(course.Modules, r.MinLen[Module](1), // At least one module r.Every(func(fs u.FieldState[Module]) error { module := fs.Value moduleSchema := u.Schema{ "title": u.Field(module.Title, r.NotZero, r.MinS(3)), "lessons": u.Field(module.Lessons, r.MinLen[Lesson](1), // At least one lesson per module r.Every(func(fs u.FieldState[Lesson]) error { lesson := fs.Value lessonSchema := u.Schema{ "title": u.Field(lesson.Title, r.NotZero, r.MinS(3)), "duration": u.Field(lesson.Duration, r.MinN(1), r.MaxN(240)), // 1-240 minutes "content": u.Field(lesson.Content, r.NotZero, r.MinS(10)), } return u.NewSouuup(lessonSchema).Validate() }), ), } return u.NewSouuup(moduleSchema).Validate() }), ), }
return u.NewSouuup(schema).Validate()}
Nested validation produces hierarchical error structures that match your data shape:
// Input data with multiple validation errorsuser := User{ Name: "Jo", // Too short Email: "invalid-email", // No @ symbol Address: Address{ Street: "", // Empty City: "London", Country: "USA", // Wrong length (should be 2 chars) PostCode: "12345", },}
err := ValidateUser(user)fmt.Println(err.Error())
Resulting error structure:
{ "name": { "errors": ["length is 2, but needs to be at least 2"] }, "email": { "errors": ["\"invalid-email\" does not contain \"@\", but needs to"] }, "address": { "street": { "errors": ["value is required but has zero value"] }, "country": { "errors": ["length is 3, but needs to be exactly 2"] } }}
if err != nil { if validationErr, ok := err.(*u.ValidationError); ok { errorMap := validationErr.ToMap()
// Check for specific nested errors if addressErrors, exists := errorMap["address"]; exists { if streetErrors, exists := addressErrors["street"]; exists { fmt.Println("Street validation failed:", streetErrors) } } }}
Sometimes you need to validate fields differently based on other field values:
type PaymentInfo struct { Method string // "card" or "bank" CardNumber string ExpiryDate string BankAccount string RoutingCode string}
func ValidatePaymentInfo(payment PaymentInfo) error { baseSchema := u.Schema{ "method": u.Field(payment.Method, r.NotZero, r.InS([]string{"card", "bank"}), ), }
// Add conditional validation based on payment method if payment.Method == "card" { baseSchema["card_number"] = u.Field(payment.CardNumber, r.NotZero, r.LenS(16), ) baseSchema["expiry_date"] = u.Field(payment.ExpiryDate, r.NotZero, r.LenS(5), // MM/YY format ) } else if payment.Method == "bank" { baseSchema["bank_account"] = u.Field(payment.BankAccount, r.NotZero, r.MinS(8), ) baseSchema["routing_code"] = u.Field(payment.RoutingCode, r.NotZero, r.LenS(9), ) }
return u.NewSouuup(baseSchema).Validate()}
Create reusable validation functions for common nested structures:
// Reusable address validatorfunc AddressSchema(addr Address) u.Schema { return u.Schema{ "street": u.Field(addr.Street, r.NotZero, r.MinS(5)), "city": u.Field(addr.City, r.NotZero, r.MinS(2)), "country": u.Field(addr.Country, r.NotZero, r.LenS(2)), "post_code": u.Field(addr.PostCode, r.NotZero), }}
// Reusable contact validatorfunc ContactSchema(contact ContactInfo) u.Schema { return u.Schema{ "phone": u.Field(contact.Phone, r.NotZero, r.MinS(10)), "email": u.Field(contact.Email, r.NotZero, r.ContainsS("@")), }}
// Use in multiple placesfunc ValidateCustomer(customer Customer) error { schema := u.Schema{ "name": u.Field(customer.Name, r.NotZero), "billing_address": AddressSchema(customer.BillingAddress), "shipping_address": AddressSchema(customer.ShippingAddress), "contact": ContactSchema(customer.Contact), }
return u.NewSouuup(schema).Validate()}
Here’s a comprehensive example validating a complex e-commerce order:
type Order struct { ID string Customer Customer Items []OrderItem ShippingInfo ShippingInfo PaymentInfo PaymentInfo TotalAmount float64}
type Customer struct { ID string Name string Email string}
type OrderItem struct { ProductID string Quantity int UnitPrice float64 Discount float64}
type ShippingInfo struct { Address Address Method string TrackingID string}
func ValidateOrder(order Order) error { schema := u.Schema{ "id": u.Field(order.ID, r.NotZero, r.MinS(5)),
"customer": u.Schema{ "id": u.Field(order.Customer.ID, r.NotZero), "name": u.Field(order.Customer.Name, r.NotZero, r.MinS(2)), "email": u.Field(order.Customer.Email, r.NotZero, r.ContainsS("@")), },
"items": u.Field(order.Items, r.MinLen[OrderItem](1), // At least one item r.Every(func(fs u.FieldState[OrderItem]) error { item := fs.Value itemSchema := u.Schema{ "product_id": u.Field(item.ProductID, r.NotZero), "quantity": u.Field(item.Quantity, r.MinN(1), r.MaxN(99)), "unit_price": u.Field(item.UnitPrice, r.Gt(0.0)), "discount": u.Field(item.Discount, r.MinN(0.0), r.Lte(item.UnitPrice)), } return u.NewSouuup(itemSchema).Validate() }), ),
"shipping_info": u.Schema{ "address": AddressSchema(order.ShippingInfo.Address), "method": u.Field(order.ShippingInfo.Method, r.NotZero, r.InS([]string{"standard", "express", "overnight"}), ), "tracking_id": u.Field(order.ShippingInfo.TrackingID, r.MinS(0)), },
"payment_info": PaymentSchema(order.PaymentInfo),
"total_amount": u.Field(order.TotalAmount, r.Gt(0.0)), }
return u.NewSouuup(schema).Validate()}
Mirror Your Data Structure
Keep your validation schema structure as close as possible to your data structure for clarity.
// Good: Mirrors the struct"user": u.Schema{ "profile": u.Schema{ "name": u.Field(...) }}
Use Descriptive Field Names
Use clear, descriptive names that match your domain language.
// Good"shipping_address": AddressSchema(...)"billing_address": AddressSchema(...)
// Avoid"address1": AddressSchema(...)"address2": AddressSchema(...)
Custom Rules
Learn how to create your own validation rules for domain-specific requirements.
HTTP Integration
See how to integrate nested validation with web APIs.
API Reference
Complete reference of all available functions and rules.