Multi-Source Binding
6 minute read
Learn how to bind data from multiple sources (query parameters, JSON body, headers, etc.) with configurable precedence rules.
Concept Overview
Multi-source binding allows you to populate a single struct from multiple request sources, with clear precedence rules:
graph LR
A[HTTP Request] --> B[Query Params]
A --> C[JSON Body]
A --> D[Headers]
A --> E[Path Params]
B --> F[Multi-Source Binder]
C --> F
D --> F
E --> F
F --> G[Merged Struct]
style F fill:#e1f5ff
style G fill:#d4eddaBasic Multi-Source Binding
Use binding.Auto() to bind from query, body, and headers automatically:
type UserRequest struct {
// From query or JSON body
Username string `json:"username" query:"username"`
Email string `json:"email" query:"email"`
// From header
APIKey string `header:"X-API-Key"`
}
// Works with:
// - POST /users?username=john with JSON body
// - GET /users?username=john&email=john@example.com
// - Headers: X-API-Key: secret123
req, err := binding.Auto[UserRequest](r)
Custom Multi-Source
Build custom multi-source binding with explicit precedence:
type SearchRequest struct {
Query string `query:"q" json:"query"`
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
Filters []string `json:"filters"`
SortBy string `header:"X-Sort-By" default:"created_at"`
}
// Bind from multiple sources
req, err := binding.Multi[SearchRequest](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body),
binding.WithHeaders(r.Header),
)
Precedence Rules
By default, sources are applied in order (last wins):
// Example: User ID from multiple sources
type Request struct {
UserID int `query:"user_id" json:"user_id" header:"X-User-ID"`
}
// Query: ?user_id=1
// JSON: {"user_id": 2}
// Header: X-User-ID: 3
// Default precedence (last wins):
req, err := binding.Multi[Request](
binding.WithQuery(r.URL.Query()), // user_id = 1
binding.WithJSON(r.Body), // user_id = 2 (overwrites)
binding.WithHeaders(r.Header), // user_id = 3 (overwrites)
)
// Result: user_id = 3
First-Wins Precedence
Use WithMergeStrategy to prefer first non-empty value:
req, err := binding.Multi[Request](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.WithQuery(r.URL.Query()), // user_id = 1
binding.WithJSON(r.Body), // user_id = 2 (ignored)
binding.WithHeaders(r.Header), // user_id = 3 (ignored)
)
// Result: user_id = 1
Partial Binding
Different fields can come from different sources:
type CompleteRequest struct {
// Pagination from query
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
// Search criteria from JSON body
Filters struct {
Category string `json:"category"`
Tags []string `json:"tags"`
MinPrice float64 `json:"min_price"`
MaxPrice float64 `json:"max_price"`
} `json:"filters"`
// Auth from headers
APIKey string `header:"X-API-Key"`
RequestID string `header:"X-Request-ID"`
}
// POST /search?page=2&page_size=50
// Headers: X-API-Key: secret, X-Request-ID: req-123
// Body: {"filters": {"category": "electronics", "tags": ["sale"]}}
req, err := binding.Multi[CompleteRequest](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body),
binding.WithHeaders(r.Header),
)
Path Parameters
Combine with router path parameters:
type UserUpdateRequest struct {
// From path: /users/:id
UserID int `path:"id"`
// From JSON body
Username string `json:"username"`
Email string `json:"email"`
// From header
APIKey string `header:"X-API-Key"`
}
// With gorilla/mux or chi
req, err := binding.Multi[UserUpdateRequest](
binding.WithPath(mux.Vars(r)), // or chi.URLParams(r)
binding.WithJSON(r.Body),
binding.WithHeaders(r.Header),
)
Form Data and JSON
Handle both form and JSON submissions:
type LoginRequest struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}
// Works with both:
// Content-Type: application/json
// Content-Type: application/x-www-form-urlencoded
req, err := binding.Auto[LoginRequest](r)
Source-Specific Options
Apply options to specific sources:
req, err := binding.Multi[Request](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body,
binding.WithMaxBytes(1024*1024),
binding.WithDisallowUnknownFields(),
),
binding.WithHeaders(r.Header),
)
Conditional Sources
Bind from sources based on conditions:
func BindRequest[T any](r *http.Request) (T, error) {
sources := []binding.Source{
binding.WithQuery(r.URL.Query()),
}
// Add JSON source only for POST/PUT/PATCH
if r.Method != "GET" && r.Method != "DELETE" {
sources = append(sources, binding.WithJSON(r.Body))
}
// Add auth header if present
if r.Header.Get("Authorization") != "" {
sources = append(sources, binding.WithHeaders(r.Header))
}
return binding.Multi[T](sources...)
}
Complex Example
Real-world multi-source scenario:
type ProductSearchRequest struct {
// Query parameters (user input)
Query string `query:"q"`
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
SortBy string `query:"sort_by" default:"relevance"`
// Advanced filters (JSON body)
Filters struct {
Categories []string `json:"categories"`
Brands []string `json:"brands"`
MinPrice float64 `json:"min_price"`
MaxPrice float64 `json:"max_price"`
InStock *bool `json:"in_stock"`
Rating *int `json:"min_rating"`
} `json:"filters"`
// Request metadata (headers)
Locale string `header:"Accept-Language" default:"en-US"`
Currency string `header:"X-Currency" default:"USD"`
UserAgent string `header:"User-Agent"`
RequestID string `header:"X-Request-ID"`
// Internal fields (not from request)
UserID int `binding:"-"` // Set after auth
RequestedAt time.Time `binding:"-"`
}
func SearchProducts(w http.ResponseWriter, r *http.Request) {
// Bind from multiple sources
req, err := binding.Multi[ProductSearchRequest](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body, binding.WithMaxBytes(1024*1024)),
binding.WithHeaders(r.Header),
)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Set internal fields
req.UserID = getUserID(r)
req.RequestedAt = time.Now()
// Execute search
results := executeSearch(req)
json.NewEncoder(w).Encode(results)
}
Error Handling
Multi-source errors include source information:
req, err := binding.Multi[Request](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body),
)
if err != nil {
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
log.Printf("Source: %s, Field: %s, Error: %v",
bindErr.Source, bindErr.Field, bindErr.Err)
}
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
Source Priority Pattern
Common pattern for API versioning and backward compatibility:
type VersionedRequest struct {
// Prefer header, fallback to query
APIVersion string `header:"X-API-Version" query:"api_version" default:"v1"`
// Prefer body, fallback to query
UserID int `json:"user_id" query:"user_id"`
}
// With first-wins strategy:
req, err := binding.Multi[VersionedRequest](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.WithHeaders(r.Header), // Highest priority
binding.WithQuery(r.URL.Query()), // Fallback
binding.WithJSON(r.Body), // Lowest priority
)
Middleware Pattern
Create reusable binding middleware:
func BindMiddleware[T any](next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req, err := binding.Multi[T](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body),
binding.WithHeaders(r.Header),
)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Store in context
ctx := context.WithValue(r.Context(), "request", req)
next(w, r.WithContext(ctx))
}
}
// Usage
http.HandleFunc("/users", BindMiddleware[CreateUserRequest](CreateUserHandler))
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
req := r.Context().Value("request").(CreateUserRequest)
// Use req
}
Integration with Rivaas Router
Seamless integration with rivaas.dev/router:
import (
"rivaas.dev/binding"
"rivaas.dev/router"
)
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
APIKey string `header:"X-API-Key"`
}
r := router.New()
r.POST("/users", func(c *router.Context) error {
req, err := binding.Multi[CreateUserRequest](
binding.WithJSON(c.Request().Body),
binding.WithHeaders(c.Request().Header),
)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
// Use req
return c.JSON(http.StatusCreated, createUser(req))
})
Performance Considerations
- Source order: Most specific first (headers before query)
- Lazy evaluation: Sources are processed in order
- Caching: Struct info is cached across requests
- Zero allocation: Primitive types use no extra memory
Best Practices
1. Document Source Expectations
// SearchRequest accepts search parameters from multiple sources:
// - Query: pagination (page, page_size)
// - JSON body: filters (categories, price range)
// - Headers: locale, currency
type SearchRequest struct {
// ...
}
2. Use Defaults Wisely
type Request struct {
Page int `query:"page" default:"1"` // Good
Sort string `header:"X-Sort" query:"sort" default:"created_at"` // Good
}
3. Validate After Binding
req, err := binding.Multi[Request](...)
if err != nil {
return err
}
// Validate business rules
if err := validation.Validate(req); err != nil {
return err
}
Troubleshooting
Values Not Merging
Check tag names match across sources:
// Wrong - different tag names
type Request struct {
ID int `query:"id" json:"user_id"` // Won't merge
}
// Correct - same semantic field
type Request struct {
ID int `query:"id" json:"id"`
}
Unexpected Overwrites
Use first-wins strategy or check source order:
// Last wins (default)
binding.Multi[T](
binding.WithQuery(...), // Applied first
binding.WithJSON(...), // May overwrite query
)
// First wins (explicit)
binding.Multi[T](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.WithHeaders(...), // Highest priority
binding.WithQuery(...),
)
Next Steps
- Learn about Struct Tags syntax
- Explore Error Handling strategies
- See Advanced Usage for custom sources
- Review Examples for complete patterns
For complete API details, see API Reference.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.