echo-contrib
echo-contrib copied to clipboard
add formbind package for nested struct form data binding
Add formbind package for nested form data binding
https://github.com/labstack/echo-contrib/issues/137
Summary
Introduces a new formbind package to echo-contrib that provides comprehensive support for binding nested structs, arrays, and pointer fields from form data. This enhancement allows developers to seamlessly bind complex, deeply nested web form data (such as user.profile.address.street=123 Main St or items[0].name=Product A) directly to Go structs with nested slices, pointer fields, and multiple levels of depth.
Changes
- New formbind package: Implements a complete form binding solution with recursive parsing and binding capabilities
- Nested struct support: Handles dot notation for nested field access (e.g.,
user.profile.name) - Array/slice binding: Supports array indices with bracket notation (e.g.,
items[0].name,users[1].email) - Pointer field handling: Automatically initializes nil pointers and binds values to pointer fields
- Group-based array parsing: Implements intelligent array grouping to prevent memory exhaustion attacks from large indices
- Comprehensive type support: Handles all basic Go types (int, string, bool, float, time.Time) plus complex nested structures
- Security features: Includes protection against memory exhaustion via maximum slice index limits
- Error handling: Provides detailed error reporting with field-specific error context
Key Features
Nested Structure Binding
type User struct {
Name string `form:"name"`
Profile Profile `form:"profile"`
}
type Profile struct {
Email string `form:"email"`
Address Address `form:"address"`
}
// Binds: profile.address.street=123 Main St
Array and Slice Support
type Team struct {
Name string `form:"name"`
Members []Member `form:"members"`
}
// Binds: members[0].name=Alice&members[1].name=Bob
Pointer Field Support
type User struct {
Name string `form:"name"`
Profile *Profile `form:"profile"`
}
// Automatically initializes nil pointers
Security Features
- Maximum slice index protection (prevents
items[999999999]attacks) - Group-based array parsing creates compact arrays regardless of sparse indices
- Input validation and sanitization
Benefits
- Enhanced developer experience: Eliminates manual form data parsing for complex structures
- Security focused: Built-in protection against common form-based attacks
- Type safety: Leverages Go's type system for safe form data binding
- Performance optimized: Efficient parsing with minimal memory allocation
- Backward compatible: Does not affect existing Echo functionality
- Comprehensive testing: 100% test coverage with extensive edge case handling
Test Plan
- [x] 100% test coverage: All statements covered by comprehensive test suite
- [x] Linting passes: All staticcheck and go vet checks pass without warnings
- [x] Race condition testing: No data races detected with
go test -race - [x] Edge case coverage: Extensive testing of error conditions, malformed input, and security scenarios
- [x] Manual verification: Tested with real form data scenarios including:
- Deeply nested structures (4+ levels)
- Arrays with sparse indices
- Mixed pointer and value types
- Complex real-world form structures
- [x] Security testing: Verified protection against memory exhaustion and malformed input
- [x] Performance testing: Confirmed efficient parsing of large form datasets
Examples
Basic Nested Binding
formData := url.Values{
"name": {"John Doe"},
"address.street": {"123 Main St"},
"address.city": {"Tokyo"},
}
var user User
err := formbind.Bind(&user, formData)
// user.Address.Street = "123 Main St"
// user.Address.City = "Tokyo"
Array Binding
formData := url.Values{
"items[0].name": {"Product A"},
"items[0].price": {"100"},
"items[1].name": {"Product B"},
"items[1].price": {"200"},
}
var order Order
err := formbind.Bind(&order, formData)
// order.Items[0] = {Name: "Product A", Price: 100}
// order.Items[1] = {Name: "Product B", Price: 200}
Complex Nested Arrays
formData := url.Values{
"teams[0].members[0].name": {"Alice"},
"teams[0].members[1].name": {"Bob"},
"teams[1].members[0].name": {"Charlie"},
}
var tournament Tournament
err := formbind.Bind(&tournament, formData)
// Correctly parses multi-dimensional nested structures
@aldas If possible, I would be grateful if you could review this PR. I have also addressed the parts that were reviewed on https://github.com/labstack/echo/pull/2834.