Skip to content

Nested Validation

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 structure
userSchema := 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()
}

Nested validation produces hierarchical error structures that match your data shape:

// Input data with multiple validation errors
user := 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 validator
func 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 validator
func 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 places
func 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.

Custom Rules →

HTTP Integration

See how to integrate nested validation with web APIs.

Examples →

API Reference

Complete reference of all available functions and rules.

API Reference →