Description
Universal Request Binding: ctx.Bind().All()
Overview
This proposal suggests adding a new All()
binding method to the Fiber framework that would bind data from multiple request sources (query parameters, body, URL parameters, headers, cookies) into a single struct using a single method call.
Motivation
Currently, developers need to call multiple binding methods (c.BodyParser(), c.QueryParser(), etc.) to gather data from different sources. This leads to repetitive code and requires developers to manually handle conflicts when the same field might be present in multiple sources.
Proposed API
// All binds data from multiple sources into a single struct
func (b *Bind) All(out any) error
Usage Examples
Basic Usage
type User struct {
// Field can be bound from any source
ID int `param:"id" query:"id" json:"id" form:"id"`
// Fields with source-specific tags
Name string `query:"name" json:"name" form:"name"`
Email string `json:"email" form:"email"`
Role string `header:"x-user-role"`
SessionID string `cookie:"session_id"`
// File upload support
Avatar *multipart.FileHeader `form:"avatar"`
}
app.Post("/users/:id", func(c fiber.Ctx) error {
user := new(User)
if err := c.Bind().All(user); err != nil {
return err
}
// All available data is now bound to the user struct
return c.JSON(user)
})
With Validation
type CreateProductRequest struct {
Name string `json:"name" query:"name" validate:"required,min=3"`
Price float64 `json:"price" query:"price" validate:"required,gt=0"`
CategoryID int `json:"category_id" param:"category_id" validate:"required"`
}
app.Post("/categories/:category_id/products", func(c fiber.Ctx) error {
req := new(CreateProductRequest)
if err := c.Bind().All(req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request data",
"details": err.Error(),
})
}
// Input is bound and validated
return createProduct(c, req)
})
Source Precedence
When the same field is present in multiple sources, we need a clear precedence rule. Proposed default precedence order (highest to lowest):
- URL Parameters (
:param
) - Body (JSON/XML/Form)
- Query parameters
- Headers
- Cookies
This order follows a general principle of specificity (URL param is most specific to this request), followed by explicit data (body), then ambient request data (query, headers, cookies).
Custom Source Precedence
For flexibility, developers can define custom precedence with an optional binding_source
tag:
type Product struct {
// Override default precedence (query takes precedence over body)
ID int `param:"id" query:"id" json:"id" binding_source:"param,query,body"`
// Only bind from form data, ignore other sources
Image *multipart.FileHeader `form:"image" binding_source:"form"`
}
Empty Values Handling
By default, empty values from higher-precedence sources won't overwrite non-empty values from lower-precedence sources.
Example: If a field has value "foo" in the body but an empty string in the query, the final value would be "foo".
This behavior can be changed with a configuration option:
// Override empty values behavior
if err := c.Bind().WithOverrideEmptyValues(true).All(user); err != nil {
return err
}
Implementation Details
Integration with Existing Bind Interface
The proposed feature would extend the existing Bind
interface:
type Bind interface {
// Existing methods
Body(out any) error
Query(out any) error
Params(out any) error
Headers(out any) error
Cookies(out any) error
// New methods
All(out any) error
WithOverrideEmptyValues(override bool) Bind
}
Internal Implementation Approach
- Parse each data source independently (reusing existing binding logic)
- Apply values to the output struct according to precedence rules
- Handle special cases like file uploads appropriately
- Provide detailed error messages for binding failures
// Pseudo-implementation
func (b *Bind) All(out any) error {
// Store binding errors to report after all attempts
var bindingErrors []error
// Get values from each source
paramValues, paramErr := b.getParamValues()
if paramErr != nil {
bindingErrors = append(bindingErrors, paramErr)
}
bodyValues, bodyErr := b.getBodyValues()
if bodyErr != nil {
bindingErrors = append(bindingErrors, bodyErr)
}
queryValues, queryErr := b.getQueryValues()
if queryErr != nil {
bindingErrors = append(bindingErrors, queryErr)
}
// ... similar for headers and cookies
// Apply values according to precedence
if err := b.applyValues(out, paramValues, bodyValues, queryValues, headerValues, cookieValues); err != nil {
return err
}
// If we had any binding errors but still managed to bind some values, report them
if len(bindingErrors) > 0 {
return fmt.Errorf("partial binding completed with errors: %v", bindingErrors)
}
return nil
}
Performance Considerations
- Avoid unnecessary parsing (lazy-load each source)
- Reuse existing binding mechanisms where possible
- Minimize allocations for property mapping
- Consider caching struct field metadata for repeated bindings
Benefits
- Simplified API for handling data from multiple sources
- Reduced code duplication in handlers
- Clearer intent in struct definitions
- Consistent handling of conflicts between sources
- Better integration with validation libraries
- Aligns with RESTful practices where resources can be identified and manipulated through multiple means
Future Extensions
- Support for binding to nested structs from different sources
- Custom binding functions for specific fields
- Built-in type conversion for common cases (string to int, etc.)
- Integration with schema validation
This proposal aligns with Fiber's design principles of Express.js compatibility while offering Go-idiomatic features that improve developer experience.
References
- Related issue: 🚀 v3 Request: Add Support for
*multipart.FileHeader
fields to Multipart Binder #2002 (the multipart file binding issue) - Current binding implementation in Fiber v2.x
Requested by: @ReneWerner87
Metadata
Metadata
Assignees
Type
Projects
Status