Binding Package
Complete API reference for the rivaas.dev/binding package
Complete API reference documentation for the rivaas.dev/binding package - high-performance request data binding for Go web applications.
Overview
The binding package provides a high-performance, type-safe way to bind request data from various sources (query parameters, JSON bodies, headers, etc.) into Go structs using struct tags.
import "rivaas.dev/binding"
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Age int `json:"age"`
}
// Generic API (preferred)
user, err := binding.JSON[CreateUserRequest](body)
Key Features
- Type-Safe Generic API: Compile-time type safety with zero runtime overhead
- Multiple Sources: Query, path, form, header, cookie, JSON, XML, YAML, TOML, MessagePack, Protocol Buffers
- Zero Allocation: Struct reflection info cached for optimal performance
- Flexible Type Support: Primitives, time types, collections, nested structs, custom types
- Detailed Errors: Field-level error information with context
- Extensible: Custom type converters and value getters
- Multi-Source Binding: Combine data from multiple sources with precedence control
Package Structure
graph TB
A[binding] --> B[Core API]
A --> C[Sub-Packages]
B --> B1[JSON/XML/Form]
B --> B2[Query/Header/Cookie]
B --> B3[Multi-Source]
B --> B4[Custom Binders]
C --> C1[yaml]
C --> C2[toml]
C --> C3[msgpack]
C --> C4[proto]
style A fill:#e1f5ff
style B fill:#fff3cd
style C fill:#d4eddaQuick Navigation
API Documentation
Learning Resources
Core API
Generic Functions
Type-safe binding with compile-time guarantees:
// JSON binding
func JSON[T any](data []byte, opts ...Option) (T, error)
// Query parameter binding
func Query[T any](values url.Values, opts ...Option) (T, error)
// Form data binding
func Form[T any](values url.Values, opts ...Option) (T, error)
// Header binding
func Header[T any](headers http.Header, opts ...Option) (T, error)
// Cookie binding
func Cookie[T any](cookies []*http.Cookie, opts ...Option) (T, error)
// Path parameter binding
func Path[T any](params map[string]string, opts ...Option) (T, error)
// XML binding
func XML[T any](data []byte, opts ...Option) (T, error)
// Multi-source binding
func Bind[T any](sources ...Source) (T, error)
Non-Generic Functions
For cases where type comes from a variable:
// JSON binding to pointer
func JSONTo(data []byte, target interface{}, opts ...Option) error
// Query binding to pointer
func QueryTo(values url.Values, target interface{}, opts ...Option) error
// ... similar for other sources
Reader Variants
Stream from io.Reader for large payloads:
func JSONReader[T any](r io.Reader, opts ...Option) (T, error)
func XMLReader[T any](r io.Reader, opts ...Option) (T, error)
Type System
Built-in Type Support
| Category | Types |
|---|
| Primitives | string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool |
| Time | time.Time, time.Duration |
| Network | net.IP, net.IPNet, url.URL |
| Regex | regexp.Regexp |
| Collections | []T, map[string]T |
| Pointers | *T for any supported type |
| Nested | Nested structs with dot notation |
Custom Types
Register custom converters for unsupported types:
import "github.com/google/uuid"
binder := binding.MustNew(
binding.WithConverter[uuid.UUID](uuid.Parse),
)
Control binding behavior with struct tags:
| Tag | Purpose | Example |
|---|
json | JSON body field | json:"field_name" |
query | Query parameter | query:"param_name" |
form | Form data | form:"field_name" |
header | HTTP header | header:"X-Header-Name" |
path | Path parameter | path:"param_name" |
cookie | HTTP cookie | cookie:"cookie_name" |
default | Default value | default:"value" |
validate | Validation rules | validate:"required,email" |
Error Types
BindError
Field-specific binding error:
type BindError struct {
Field string // Field name
Source string // Source ("query", "json", etc.)
Value string // Raw value
Type string // Expected type
Reason string // Error reason
Err error // Underlying error
}
UnknownFieldError
Unknown fields in strict mode:
type UnknownFieldError struct {
Fields []string // List of unknown fields
}
MultiError
Multiple errors with WithAllErrors():
type MultiError struct {
Errors []*BindError
}
Configuration Options
Common options for all binding functions:
// Security limits
binding.WithMaxDepth(16) // Max struct nesting
binding.WithMaxSliceLen(1000) // Max slice elements
binding.WithMaxMapSize(500) // Max map entries
// Unknown fields
binding.WithStrictJSON() // Fail on unknown fields
binding.WithUnknownFields(mode) // UnknownError/UnknownWarn/UnknownIgnore
// Slice parsing
binding.WithSliceMode(mode) // SliceRepeat or SliceCSV
// Error collection
binding.WithAllErrors() // Collect all errors instead of failing on first
Reusable Binders
Create configured binder instances:
binder := binding.MustNew(
binding.WithConverter[uuid.UUID](uuid.Parse),
binding.WithTimeLayouts("2006-01-02", "01/02/2006"),
binding.WithMaxDepth(16),
)
// Use across handlers
user, err := binder.JSON[User](body)
params, err := binder.Query[Params](values)
Sub-Packages
Additional format support via sub-packages:
| Package | Format | Import Path |
|---|
yaml | YAML | rivaas.dev/binding/yaml |
toml | TOML | rivaas.dev/binding/toml |
msgpack | MessagePack | rivaas.dev/binding/msgpack |
proto | Protocol Buffers | rivaas.dev/binding/proto |
- First binding: ~500ns overhead for reflection
- Subsequent bindings: ~50ns overhead (cache lookup)
- Query/Path/Form: Zero allocations for primitive types
- JSON/XML: Allocations depend on
encoding/json and encoding/xml - Thread-safe: All operations are safe for concurrent use
Integration
With net/http
func Handler(w http.ResponseWriter, r *http.Request) {
req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Process request...
}
With rivaas.dev/router
import "rivaas.dev/router"
func Handler(c *router.Context) error {
req, err := binding.JSON[CreateUserRequest](c.Request().Body)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
return c.JSON(http.StatusOK, processRequest(req))
}
With rivaas.dev/app
import "rivaas.dev/app"
func Handler(c *app.Context) error {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
return err // Automatically handled
}
return c.JSON(http.StatusOK, processRequest(req))
}
Version Compatibility
The binding package follows semantic versioning:
- v1.x: Stable API, backward compatible
- v2.x: Major changes, may require code updates
External Links
See Also
For step-by-step guides and tutorials, see the Binding Guide.
For real-world examples, see the Examples page.
1 - API Reference
Complete API documentation for all types, functions, and interfaces
Detailed API reference for all exported types, functions, and interfaces in the rivaas.dev/binding package.
Core Binding Functions
Generic API
JSON
func JSON[T any](data []byte, opts ...Option) (T, error)
Binds JSON data to a struct of type T.
Parameters:
data: JSON bytes to parseopts: Optional configuration options
Returns:
- Populated struct of type
T - Error if binding fails
Example:
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
user, err := binding.JSON[User](jsonData)
JSONReader
func JSONReader[T any](r io.Reader, opts ...Option) (T, error)
Binds JSON from an io.Reader. More memory-efficient for large payloads.
Example:
user, err := binding.JSONReader[User](r.Body)
Query
func Query[T any](values url.Values, opts ...Option) (T, error)
Binds URL query parameters to a struct.
Parameters:
values: URL query values (r.URL.Query())opts: Optional configuration options
Example:
type Params struct {
Page int `query:"page" default:"1"`
Limit int `query:"limit" default:"20"`
Tags []string `query:"tags"`
}
params, err := binding.Query[Params](r.URL.Query())
func Form[T any](values url.Values, opts ...Option) (T, error)
Binds form data to a struct.
Parameters:
values: Form values (r.Form or r.PostForm)opts: Optional configuration options
Example:
type LoginForm struct {
Username string `form:"username"`
Password string `form:"password"`
}
form, err := binding.Form[LoginForm](r.PostForm)
func Header[T any](headers http.Header, opts ...Option) (T, error)
Binds HTTP headers to a struct.
Example:
type Headers struct {
APIKey string `header:"X-API-Key"`
RequestID string `header:"X-Request-ID"`
}
headers, err := binding.Header[Headers](r.Header)
Cookie
func Cookie[T any](cookies []*http.Cookie, opts ...Option) (T, error)
Binds HTTP cookies to a struct.
Example:
type Cookies struct {
SessionID string `cookie:"session_id"`
Theme string `cookie:"theme" default:"light"`
}
cookies, err := binding.Cookie[Cookies](r.Cookies())
Path
func Path[T any](params map[string]string, opts ...Option) (T, error)
Binds URL path parameters to a struct.
Example:
type PathParams struct {
UserID int `path:"user_id"`
}
// With gorilla/mux or chi
params, err := binding.Path[PathParams](mux.Vars(r))
XML
func XML[T any](data []byte, opts ...Option) (T, error)
Binds XML data to a struct.
Example:
type Document struct {
Title string `xml:"title"`
Body string `xml:"body"`
}
doc, err := binding.XML[Document](xmlData)
XMLReader
func XMLReader[T any](r io.Reader, opts ...Option) (T, error)
Binds XML from an io.Reader.
Bind (Multi-Source)
func Bind[T any](sources ...Source) (T, error)
Binds from multiple sources with precedence.
Example:
type Request struct {
UserID int `query:"user_id" json:"user_id"`
APIKey string `header:"X-API-Key"`
}
req, err := binding.Bind[Request](
binding.FromQuery(r.URL.Query()),
binding.FromJSON(r.Body),
binding.FromHeader(r.Header),
)
Non-Generic API
JSONTo
func JSONTo(data []byte, target interface{}, opts ...Option) error
Binds JSON to a pointer. Use when type comes from a variable.
Example:
var user User
err := binding.JSONTo(jsonData, &user)
Similar non-generic functions exist for all sources:
QueryTo(values url.Values, target interface{}, opts ...Option) errorFormTo(values url.Values, target interface{}, opts ...Option) errorHeaderTo(headers http.Header, target interface{}, opts ...Option) errorCookieTo(cookies []*http.Cookie, target interface{}, opts ...Option) errorPathTo(params map[string]string, target interface{}, opts ...Option) errorXMLTo(data []byte, target interface{}, opts ...Option) error
Source Constructors
For multi-source binding:
func FromJSON(r io.Reader) Source
func FromQuery(values url.Values) Source
func FromForm(values url.Values) Source
func FromHeader(headers http.Header) Source
func FromCookie(cookies []*http.Cookie) Source
func FromPath(params map[string]string) Source
func FromXML(r io.Reader) Source
Binder Type
Constructor
func New(opts ...Option) (*Binder, error)
func MustNew(opts ...Option) *Binder
Creates a reusable binder with configuration.
Example:
binder := binding.MustNew(
binding.WithConverter[uuid.UUID](uuid.Parse),
binding.WithMaxDepth(16),
)
user, err := binder.JSON[User](data)
Binder Methods
A Binder has the same methods as the package-level functions:
func (b *Binder) JSON[T any](data []byte, opts ...Option) (T, error)
func (b *Binder) Query[T any](values url.Values, opts ...Option) (T, error)
// ... etc for all binding functions
Error Types
BindError
Field-specific binding error with detailed context:
type BindError struct {
Field string // Field name that failed to bind
Source string // Source ("query", "json", "header", etc.)
Value string // Raw value that failed to bind
Type string // Expected Go type
Reason string // Human-readable reason
Err error // Underlying error
}
func (e *BindError) Error() string
func (e *BindError) Unwrap() error
func (e *BindError) IsType() bool // True if type conversion failed
func (e *BindError) IsMissing() bool // True if required field missing
Example:
user, err := binding.JSON[User](data)
if err != nil {
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
log.Printf("Field %s from %s failed: %v",
bindErr.Field, bindErr.Source, bindErr.Err)
}
}
UnknownFieldError
Returned in strict mode when unknown fields are encountered:
type UnknownFieldError struct {
Fields []string // List of unknown field names
}
func (e *UnknownFieldError) Error() string
Example:
user, err := binding.JSON[User](data, binding.WithStrictJSON())
if err != nil {
var unknownErr *binding.UnknownFieldError
if errors.As(err, &unknownErr) {
log.Printf("Unknown fields: %v", unknownErr.Fields)
}
}
MultiError
Multiple errors collected with WithAllErrors():
type MultiError struct {
Errors []*BindError
}
func (e *MultiError) Error() string
func (e *MultiError) Unwrap() []error
Example:
user, err := binding.JSON[User](data, binding.WithAllErrors())
if err != nil {
var multi *binding.MultiError
if errors.As(err, &multi) {
for _, e := range multi.Errors {
log.Printf("Field %s: %v", e.Field, e.Err)
}
}
}
Interfaces
ValueGetter
Interface for custom data sources:
type ValueGetter interface {
Get(key string) string // Get first value for key
GetAll(key string) []string // Get all values for key
Has(key string) bool // Check if key exists
}
Example Implementation:
type EnvGetter struct{}
func (g *EnvGetter) Get(key string) string {
return os.Getenv(key)
}
func (g *EnvGetter) GetAll(key string) []string {
if val := os.Getenv(key); val != "" {
return []string{val}
}
return nil
}
func (g *EnvGetter) Has(key string) bool {
_, exists := os.LookupEnv(key)
return exists
}
ConverterFunc
Function type for custom type converters:
type ConverterFunc[T any] func(string) (T, error)
Example:
func ParseEmail(s string) (Email, error) {
if !strings.Contains(s, "@") {
return "", errors.New("invalid email")
}
return Email(s), nil
}
binder := binding.MustNew(
binding.WithConverter[Email](ParseEmail),
)
Helper Functions
MapGetter
Converts a map[string]string to a ValueGetter:
func MapGetter(m map[string]string) ValueGetter
Example:
data := map[string]string{"name": "Alice", "age": "30"}
getter := binding.MapGetter(data)
result, err := binding.RawInto[User](getter, "custom")
MultiMapGetter
Converts a map[string][]string to a ValueGetter:
func MultiMapGetter(m map[string][]string) ValueGetter
Example:
data := map[string][]string{
"tags": {"go", "rust"},
"name": {"Alice"},
}
getter := binding.MultiMapGetter(data)
result, err := binding.RawInto[User](getter, "custom")
GetterFunc
Adapts a function to the ValueGetter interface:
type GetterFunc func(key string) ([]string, bool)
func (f GetterFunc) Get(key string) string
func (f GetterFunc) GetAll(key string) []string
func (f GetterFunc) Has(key string) bool
Example:
getter := binding.GetterFunc(func(key string) ([]string, bool) {
if val, ok := myMap[key]; ok {
return []string{val}, true
}
return nil, false
})
Raw/RawInto
Low-level binding from custom ValueGetter:
func Raw[T any](getter ValueGetter, source string, opts ...Option) (T, error)
func RawInto(getter ValueGetter, source string, target interface{}, opts ...Option) error
Events and Observability
Events Type
Hooks for observing binding operations:
type Events struct {
FieldBound func(name, tag string)
UnknownField func(name string)
Done func(stats Stats)
}
Example:
binder := binding.MustNew(
binding.WithEvents(binding.Events{
FieldBound: func(name, tag string) {
log.Printf("Bound field %s from %s", name, tag)
},
UnknownField: func(name string) {
log.Printf("Unknown field: %s", name)
},
Done: func(stats binding.Stats) {
log.Printf("Binding completed: %d fields, %d errors",
stats.FieldsBound, stats.ErrorCount)
},
}),
)
Stats Type
Statistics from binding operation:
type Stats struct {
FieldsBound int // Number of fields successfully bound
ErrorCount int // Number of errors encountered
Duration time.Duration // Time taken for binding
}
Constants
Slice Modes
const (
SliceRepeat SliceMode = iota // Repeated params: ?tags=a&tags=b (default)
SliceCSV // CSV params: ?tags=a,b,c
)
Unknown Field Handling
const (
UnknownIgnore UnknownMode = iota // Ignore unknown fields (default)
UnknownWarn // Log warning for unknown fields
UnknownError // Error on unknown fields
)
Merge Strategies
const (
MergeLastWins MergeStrategy = iota // Last source wins (default)
MergeFirstWins // First source wins
)
Default Values
Time Layouts
var DefaultTimeLayouts = []string{
time.RFC3339,
time.RFC3339Nano,
time.RFC1123,
time.RFC1123Z,
time.RFC822,
time.RFC822Z,
time.RFC850,
time.ANSIC,
time.UnixDate,
time.RubyDate,
time.Kitchen,
time.Stamp,
time.StampMilli,
time.StampMicro,
time.StampNano,
time.DateTime,
time.DateOnly,
time.TimeOnly,
"2006-01-02",
"01/02/2006",
"2006/01/02",
}
Can be extended with WithTimeLayouts().
Type Constraints
Supported Interface Types
Types implementing these interfaces are automatically supported:
encoding.TextUnmarshaler: For custom text unmarshalingjson.Unmarshaler: For custom JSON unmarshalingxml.Unmarshaler: For custom XML unmarshaling
Example:
type Status string
func (s *Status) UnmarshalText(text []byte) error {
// Custom parsing logic
*s = Status(string(text))
return nil
}
type Request struct {
Status Status `query:"status"` // Automatically uses UnmarshalText
}
Thread Safety
All package-level functions and Binder methods are safe for concurrent use. The struct reflection cache is thread-safe and has no size limit.
See Also
For usage examples, see the Binding Guide.
2 - Options
Complete reference for all configuration options
Comprehensive reference for all configuration options available in the binding package.
Option Type
type Option func(*Config)
Options configure binding behavior. They can be passed to:
- Package-level functions (e.g.,
binding.JSON[T](data, opts...)) Binder constructor (e.g., binding.MustNew(opts...))Binder methods (e.g., binder.JSON[T](data, opts...))
Security Limits
WithMaxDepth
func WithMaxDepth(depth int) Option
Sets maximum struct nesting depth to prevent stack overflow from deeply nested structures.
Default: 32
Example:
user, err := binding.JSON[User](data, binding.WithMaxDepth(16))
Use Cases:
- Protect against malicious deeply nested JSON
- Limit resource usage
- Prevent stack overflow
WithMaxSliceLen
func WithMaxSliceLen(length int) Option
Sets maximum slice length to prevent memory exhaustion from large arrays.
Default: 10,000
Example:
params, err := binding.Query[Params](values, binding.WithMaxSliceLen(1000))
Use Cases:
- Protect against memory attacks
- Limit array sizes
- Control memory allocation
WithMaxMapSize
func WithMaxMapSize(size int) Option
Sets maximum map size to prevent memory exhaustion from large objects.
Default: 1,000
Example:
config, err := binding.JSON[Config](data, binding.WithMaxMapSize(500))
Use Cases:
- Protect against memory attacks
- Limit object sizes
- Control memory allocation
Unknown Field Handling
WithStrictJSON
func WithStrictJSON() Option
Convenience function that sets WithUnknownFields(UnknownError). Fails binding if JSON contains fields not in the struct.
Example:
user, err := binding.JSON[User](data, binding.WithStrictJSON())
if err != nil {
var unknownErr *binding.UnknownFieldError
if errors.As(err, &unknownErr) {
log.Printf("Unknown fields: %v", unknownErr.Fields)
}
}
Use Cases:
- API versioning
- Catch typos in field names
- Enforce strict contracts
WithUnknownFields
func WithUnknownFields(mode UnknownMode) Option
// Modes
const (
UnknownIgnore UnknownMode = iota // Ignore unknown fields (default)
UnknownWarn // Log warnings
UnknownError // Return error
)
Controls how unknown fields are handled.
Example:
user, err := binding.JSON[User](data,
binding.WithUnknownFields(binding.UnknownWarn))
Modes:
UnknownIgnore: Silently ignore (default, most flexible)UnknownWarn: Log warnings (for debugging)UnknownError: Fail binding (strict contracts)
Slice Parsing
WithSliceMode
func WithSliceMode(mode SliceMode) Option
// Modes
const (
SliceRepeat SliceMode = iota // ?tags=a&tags=b (default)
SliceCSV // ?tags=a,b,c
)
Controls how slices are parsed from query/form values.
Example:
// URL: ?tags=go,rust,python
params, err := binding.Query[Params](values,
binding.WithSliceMode(binding.SliceCSV))
Modes:
SliceRepeat: Repeated parameters (default, standard HTTP)SliceCSV: Comma-separated values (more compact)
Error Handling
WithAllErrors
func WithAllErrors() Option
Collects all binding errors instead of failing on the first error.
Example:
user, err := binding.JSON[User](data, binding.WithAllErrors())
if err != nil {
var multi *binding.MultiError
if errors.As(err, &multi) {
for _, e := range multi.Errors {
log.Printf("Field %s: %v", e.Field, e.Err)
}
}
}
Use Cases:
- Show all validation errors to user
- Debugging
- Comprehensive error reporting
Type Conversion
WithConverter
func WithConverter[T any](fn func(string) (T, error)) Option
Registers a custom type converter for type T.
Example:
import "github.com/google/uuid"
binder := binding.MustNew(
binding.WithConverter[uuid.UUID](uuid.Parse),
)
type User struct {
ID uuid.UUID `query:"id"`
}
user, err := binder.Query[User](values)
Use Cases:
- Custom types (UUID, decimal, etc.)
- Domain-specific types
- Third-party types
WithTimeLayouts
func WithTimeLayouts(layouts ...string) Option
Sets custom time parsing layouts. Replaces default layouts.
Default Layouts: See binding.DefaultTimeLayouts
Example:
binder := binding.MustNew(
binding.WithTimeLayouts(
"2006-01-02", // Date only
"01/02/2006", // US format
"2006-01-02 15:04:05", // DateTime
),
)
Tip: Extend defaults instead of replacing:
binder := binding.MustNew(
binding.WithTimeLayouts(
append(binding.DefaultTimeLayouts, "01/02/2006", "02-Jan-2006")...,
),
)
Observability
WithEvents
func WithEvents(events Events) Option
type Events struct {
FieldBound func(name, tag string)
UnknownField func(name string)
Done func(stats Stats)
}
type Stats struct {
FieldsBound int
ErrorCount int
Duration time.Duration
}
Registers event handlers for observing binding operations.
Example:
binder := binding.MustNew(
binding.WithEvents(binding.Events{
FieldBound: func(name, tag string) {
metrics.Increment("binding.field.bound",
"field:"+name, "source:"+tag)
},
UnknownField: func(name string) {
log.Warn("Unknown field", "name", name)
},
Done: func(stats binding.Stats) {
metrics.Histogram("binding.duration",
stats.Duration.Milliseconds())
metrics.Gauge("binding.fields", stats.FieldsBound)
},
}),
)
Use Cases:
- Metrics collection
- Debugging
- Performance monitoring
- Audit logging
Multi-Source Options
WithMergeStrategy
func WithMergeStrategy(strategy MergeStrategy) Option
// Strategies
const (
MergeLastWins MergeStrategy = iota // Last source wins (default)
MergeFirstWins // First source wins
)
Controls precedence when binding from multiple sources.
Example:
// First source wins
req, err := binding.Bind[Request](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.FromHeader(r.Header), // Highest priority
binding.FromQuery(r.URL.Query()), // Lower priority
)
Strategies:
MergeLastWins: Last source overwrites (default)MergeFirstWins: First non-empty value wins
JSON-Specific Options
WithDisallowUnknownFields
func WithDisallowUnknownFields() Option
Equivalent to WithStrictJSON(). Provided for clarity when explicitly disallowing unknown fields.
Example:
user, err := binding.JSON[User](data,
binding.WithDisallowUnknownFields())
WithMaxBytes
func WithMaxBytes(bytes int64) Option
Limits the size of JSON/XML data to prevent memory exhaustion.
Example:
user, err := binding.JSON[User](data,
binding.WithMaxBytes(1024 * 1024)) // 1MB limit
Use Cases:
- Protect against large payloads
- API rate limiting
- Resource management
Custom Options
WithTagHandler
func WithTagHandler(tagName string, handler TagHandler) Option
type TagHandler interface {
Get(fieldName, tagValue string) (string, bool)
}
Registers a custom struct tag handler.
Example:
type EnvTagHandler struct {
prefix string
}
func (h *EnvTagHandler) Get(fieldName, tagValue string) (string, bool) {
envKey := h.prefix + tagValue
val, exists := os.LookupEnv(envKey)
return val, exists
}
binder := binding.MustNew(
binding.WithTagHandler("env", &EnvTagHandler{prefix: "APP_"}),
)
type Config struct {
APIKey string `env:"API_KEY"` // Looks up APP_API_KEY
}
Option Combinations
Production Configuration
var ProductionBinder = binding.MustNew(
// Security
binding.WithMaxDepth(16),
binding.WithMaxSliceLen(1000),
binding.WithMaxMapSize(500),
binding.WithMaxBytes(10 * 1024 * 1024), // 10MB
// Strict validation
binding.WithStrictJSON(),
// Custom types
binding.WithConverter[uuid.UUID](uuid.Parse),
binding.WithConverter[decimal.Decimal](decimal.NewFromString),
// Time formats
binding.WithTimeLayouts(append(
binding.DefaultTimeLayouts,
"2006-01-02",
"01/02/2006",
)...),
// Observability
binding.WithEvents(binding.Events{
FieldBound: logFieldBound,
UnknownField: logUnknownField,
Done: recordMetrics,
}),
)
Development Configuration
var DevBinder = binding.MustNew(
// Lenient limits
binding.WithMaxDepth(32),
binding.WithMaxSliceLen(10000),
// Warnings instead of errors
binding.WithUnknownFields(binding.UnknownWarn),
// Collect all errors for debugging
binding.WithAllErrors(),
// Verbose logging
binding.WithEvents(binding.Events{
FieldBound: func(name, tag string) {
log.Printf("[DEBUG] Bound %s from %s", name, tag)
},
UnknownField: func(name string) {
log.Printf("[WARN] Unknown field: %s", name)
},
Done: func(stats binding.Stats) {
log.Printf("[DEBUG] Binding: %d fields, %d errors, %v",
stats.FieldsBound, stats.ErrorCount, stats.Duration)
},
}),
)
Testing Configuration
var TestBinder = binding.MustNew(
// Strict validation
binding.WithStrictJSON(),
// Fail fast
// (don't use WithAllErrors in tests)
// Smaller limits for test data
binding.WithMaxDepth(8),
binding.WithMaxSliceLen(100),
)
Option Precedence
When options are provided to both MustNew() and individual functions:
- Function-level options override binder-level options
- Options are applied in order (last wins for same option)
Example:
binder := binding.MustNew(
binding.WithMaxDepth(32), // Binder default
)
// This call uses maxDepth=16 (overrides binder default)
user, err := binder.JSON[User](data,
binding.WithMaxDepth(16))
Best Practices
1. Use Binders for Shared Configuration
// Good - shared configuration
var AppBinder = binding.MustNew(
binding.WithConverter[uuid.UUID](uuid.Parse),
binding.WithMaxDepth(16),
)
func Handler1(r *http.Request) {
user, err := AppBinder.JSON[User](r.Body)
}
func Handler2(r *http.Request) {
params, err := AppBinder.Query[Params](r.URL.Query())
}
2. Set Security Limits
// Good - protect against attacks
user, err := binding.JSON[User](data,
binding.WithMaxDepth(16),
binding.WithMaxSliceLen(1000),
binding.WithMaxBytes(1024*1024),
)
3. Use Strict Mode for APIs
// Good - catch client errors early
user, err := binding.JSON[User](data, binding.WithStrictJSON())
// Good - show all validation errors to user
form, err := binding.Form[Form](r.PostForm, binding.WithAllErrors())
if err != nil {
var multi *binding.MultiError
if errors.As(err, &multi) {
// Show all errors to user
for _, e := range multi.Errors {
addError(e.Field, e.Err.Error())
}
}
}
See Also
For usage examples, see the Binding Guide.
3 - Sub-Packages
YAML, TOML, MessagePack, and Protocol Buffers support
Reference for sub-packages that add support for additional data formats beyond the core package.
Package Overview
| Sub-Package | Format | Import Path |
|---|
yaml | YAML | rivaas.dev/binding/yaml |
toml | TOML | rivaas.dev/binding/toml |
msgpack | MessagePack | rivaas.dev/binding/msgpack |
proto | Protocol Buffers | rivaas.dev/binding/proto |
YAML Package
Import
import "rivaas.dev/binding/yaml"
Functions
YAML
func YAML[T any](data []byte, opts ...Option) (T, error)
Binds YAML data to a struct.
Example:
type Config struct {
Name string `yaml:"name"`
Port int `yaml:"port"`
Debug bool `yaml:"debug"`
}
config, err := yaml.YAML[Config](yamlData)
YAMLReader
func YAMLReader[T any](r io.Reader, opts ...Option) (T, error)
Binds YAML from an io.Reader.
Example:
config, err := yaml.YAMLReader[Config](r.Body)
YAMLTo
func YAMLTo(data []byte, target interface{}, opts ...Option) error
Non-generic variant.
Options
WithStrict
Enables strict YAML parsing. Fails on unknown fields or duplicate keys.
Example:
config, err := yaml.YAML[Config](data, yaml.WithStrict())
Use yaml struct tags:
type Config struct {
Name string `yaml:"name"`
Port int `yaml:"port"`
Debug bool `yaml:"debug,omitempty"`
// Inline nested struct
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"database"`
// Ignore field
Internal string `yaml:"-"`
}
Example
# config.yaml
name: my-app
port: 8080
debug: true
database:
host: localhost
port: 5432
data, _ := os.ReadFile("config.yaml")
config, err := yaml.YAML[Config](data)
TOML Package
Import
import "rivaas.dev/binding/toml"
Functions
TOML
func TOML[T any](data []byte, opts ...Option) (T, error)
Binds TOML data to a struct.
Example:
type Config struct {
Name string `toml:"name"`
Port int `toml:"port"`
Debug bool `toml:"debug"`
}
config, err := toml.TOML[Config](tomlData)
TOMLReader
func TOMLReader[T any](r io.Reader, opts ...Option) (T, error)
Binds TOML from an io.Reader.
TOMLTo
func TOMLTo(data []byte, target interface{}, opts ...Option) error
Non-generic variant.
Use toml struct tags:
type Config struct {
Title string `toml:"title"`
Owner struct {
Name string `toml:"name"`
DOB time.Time `toml:"dob"`
} `toml:"owner"`
Database struct {
Server string `toml:"server"`
Ports []int `toml:"ports"`
Enabled bool `toml:"enabled"`
} `toml:"database"`
}
Example
# config.toml
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
enabled = true
data, _ := os.ReadFile("config.toml")
config, err := toml.TOML[Config](data)
MessagePack Package
Import
import "rivaas.dev/binding/msgpack"
Functions
MsgPack
func MsgPack[T any](data []byte, opts ...Option) (T, error)
Binds MessagePack data to a struct.
Example:
type Message struct {
ID int `msgpack:"id"`
Data []byte `msgpack:"data"`
Time time.Time `msgpack:"time"`
}
msg, err := msgpack.MsgPack[Message](msgpackData)
MsgPackReader
func MsgPackReader[T any](r io.Reader, opts ...Option) (T, error)
Binds MessagePack from an io.Reader.
Example:
msg, err := msgpack.MsgPackReader[Message](r.Body)
MsgPackTo
func MsgPackTo(data []byte, target interface{}, opts ...Option) error
Non-generic variant.
Use msgpack struct tags:
type Message struct {
ID int `msgpack:"id"`
Type string `msgpack:"type"`
Payload []byte `msgpack:"payload"`
Created time.Time `msgpack:"created"`
// Omit if zero
Metadata map[string]string `msgpack:"metadata,omitempty"`
// Use as array (more compact)
Points []int `msgpack:"points,as_array"`
}
Use Cases
- High-performance binary serialization
- Microservice communication
- Event streaming
- Cache serialization
Protocol Buffers Package
Import
import "rivaas.dev/binding/proto"
import pb "myapp/proto" // Your generated proto files
Functions
Proto
func Proto[T proto.Message](data []byte, opts ...Option) (T, error)
Binds Protocol Buffer data to a proto message.
Example:
import pb "myapp/proto"
user, err := proto.Proto[*pb.User](protoData)
ProtoReader
func ProtoReader[T proto.Message](r io.Reader, opts ...Option) (T, error)
Binds Protocol Buffers from an io.Reader.
Example:
user, err := proto.ProtoReader[*pb.User](r.Body)
ProtoTo
func ProtoTo(data []byte, target proto.Message, opts ...Option) error
Non-generic variant.
Proto Definition
// user.proto
syntax = "proto3";
package example;
option go_package = "myapp/proto";
message User {
int64 id = 1;
string username = 2;
string email = 3;
int32 age = 4;
repeated string tags = 5;
}
Example
import (
"rivaas.dev/binding/proto"
pb "myapp/proto"
)
func HandleProtoRequest(w http.ResponseWriter, r *http.Request) {
user, err := proto.ProtoReader[*pb.User](r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Use user
log.Printf("Received user: %s", user.Username)
}
Use Cases
- gRPC services
- High-performance APIs
- Cross-language communication
- Schema evolution
Common Patterns
Configuration Files
import (
"rivaas.dev/binding/yaml"
"rivaas.dev/binding/toml"
)
type Config struct {
Name string `yaml:"name" toml:"name"`
Port int `yaml:"port" toml:"port"`
Database struct {
Host string `yaml:"host" toml:"host"`
Port int `yaml:"port" toml:"port"`
} `yaml:"database" toml:"database"`
}
func LoadConfig(format, path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
switch format {
case "yaml", "yml":
return yaml.YAML[Config](data)
case "toml":
return toml.TOML[Config](data)
default:
return nil, fmt.Errorf("unsupported format: %s", format)
}
}
Content Negotiation
func HandleRequest(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
var req CreateUserRequest
var err error
switch {
case strings.Contains(contentType, "application/json"):
req, err = binding.JSON[CreateUserRequest](r.Body)
case strings.Contains(contentType, "application/x-yaml"):
req, err = yaml.YAMLReader[CreateUserRequest](r.Body)
case strings.Contains(contentType, "application/toml"):
req, err = toml.TOMLReader[CreateUserRequest](r.Body)
case strings.Contains(contentType, "application/x-msgpack"):
req, err = msgpack.MsgPackReader[CreateUserRequest](r.Body)
default:
http.Error(w, "Unsupported content type", http.StatusUnsupportedMediaType)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Process request...
}
type API struct {
yaml *yaml.Binder
toml *toml.Binder
msgpack *msgpack.Binder
}
func NewAPI() *API {
return &API{
yaml: yaml.MustNew(yaml.WithStrict()),
toml: toml.MustNew(),
msgpack: msgpack.MustNew(),
}
}
func (a *API) Bind(r *http.Request, target interface{}) error {
contentType := r.Header.Get("Content-Type")
switch {
case strings.Contains(contentType, "yaml"):
return a.yaml.YAMLReaderTo(r.Body, target)
case strings.Contains(contentType, "toml"):
return a.toml.TOMLReaderTo(r.Body, target)
case strings.Contains(contentType, "msgpack"):
return a.msgpack.MsgPackReaderTo(r.Body, target)
default:
return binding.JSONReaderTo(r.Body, target)
}
}
Dependencies
Sub-packages have external dependencies:
| Package | Dependency |
|---|
yaml | gopkg.in/yaml.v3 |
toml | github.com/BurntSushi/toml |
msgpack | github.com/vmihailenco/msgpack/v5 |
proto | google.golang.org/protobuf |
Install with:
# YAML
go get gopkg.in/yaml.v3
# TOML
go get github.com/BurntSushi/toml
# MessagePack
go get github.com/vmihailenco/msgpack/v5
# Protocol Buffers
go get google.golang.org/protobuf
Approximate performance for a typical struct (10 fields):
| Format | Speed (ns/op) | Allocs | Use Case |
|---|
| JSON | 800 | 3 | Web APIs, human-readable |
| MessagePack | 500 | 2 | High performance, binary |
| Protocol Buffers | 400 | 2 | Strongly typed, cross-language |
| YAML | 1,200 | 5 | Configuration files |
| TOML | 1,000 | 4 | Configuration files |
Best Practices
- JSON: Web APIs, JavaScript clients
- YAML: Configuration files, human-readable
- TOML: Configuration files, less ambiguous than YAML
- MessagePack: High-performance microservices
- Protocol Buffers: gRPC, schema evolution
All sub-packages support the same options as core binding:
config, err := yaml.YAML[Config](data,
binding.WithMaxDepth(16),
binding.WithMaxSliceLen(1000),
)
3. Stream Large Files
Use Reader variants for large payloads:
// Good - streams from disk
file, _ := os.Open("large-config.yaml")
config, err := yaml.YAMLReader[Config](file)
// Bad - loads entire file into memory
data, _ := os.ReadFile("large-config.yaml")
config, err := yaml.YAML[Config](data)
See Also
For usage examples, see the Binding Guide.
4 - Troubleshooting
Common issues, solutions, and FAQs
Solutions to common issues, frequently asked questions, and debugging strategies for the binding package.
Common Issues
Field Not Binding
Problem: Field remains zero value after binding
Possible Causes:
Field is unexported
// Wrong - unexported field
type Request struct {
name string `json:"name"` // Won't bind
}
// Correct
type Request struct {
Name string `json:"name"`
}
Tag name doesn’t match source key
// JSON: {"username": "alice"}
type Request struct {
Name string `json:"name"` // Wrong tag name
}
// Correct
type Request struct {
Name string `json:"username"` // Matches JSON key
}
Wrong tag type for source
// Binding from query parameters
type Request struct {
Name string `json:"name"` // Wrong - should be `query:"name"`
}
// Correct
type Request struct {
Name string `query:"name"`
}
Source doesn’t contain the key
// URL: ?page=1
type Params struct {
Page int `query:"page"`
Limit int `query:"limit"` // Missing in URL
}
// Solution: Use default
type Params struct {
Page int `query:"page" default:"1"`
Limit int `query:"limit" default:"20"`
}
Type Conversion Errors
Problem: Error like “cannot unmarshal string into int”
Solutions:
Check source data type
// JSON: {"age": "30"} <- string instead of number
type User struct {
Age int `json:"age"`
}
// Error: cannot unmarshal string into int
Fix: Ensure JSON sends number: {"age": 30}
Use string type and convert manually
type User struct {
AgeStr string `json:"age"`
}
user, err := binding.JSON[User](data)
age, _ := strconv.Atoi(user.AgeStr)
Register custom converter
binder := binding.MustNew(
binding.WithConverter[MyType](parseMyType),
)
Slice Not Parsing
Problem: Slice remains empty or has unexpected values
Cause: Wrong slice mode for input format
// URL: ?tags=go,rust,python
type Params struct {
Tags []string `query:"tags"`
}
// With default mode (SliceRepeat)
params, _ := binding.Query[Params](values)
// Result: Tags = ["go,rust,python"] <- Wrong!
Solution: Use CSV mode
params, err := binding.Query[Params](values,
binding.WithSliceMode(binding.SliceCSV))
// Result: Tags = ["go", "rust", "python"] <- Correct!
Or use repeated parameters:
// URL: ?tags=go&tags=rust&tags=python
params, _ := binding.Query[Params](values) // Default mode works
JSON Parsing Errors
Problem: “unexpected end of JSON input” or “invalid character”
Causes:
Malformed JSON
{"name": "test" // Missing closing brace
Solution: Validate JSON syntax
Empty body
// Body is empty but expecting JSON
user, err := binding.JSON[User](r.Body)
// Error: unexpected end of JSON input
Solution: Check if body is empty first
body, err := io.ReadAll(r.Body)
if len(body) == 0 {
return errors.New("empty body")
}
user, err := binding.JSON[User](body)
Body already consumed
body, _ := io.ReadAll(r.Body) // Consumes body
// ... some code ...
user, err := binding.JSON[User](r.Body) // Error: body empty
Solution: Restore body
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(body))
user, err := binding.JSON[User](body)
Unknown Field Errors
Problem: Error in strict mode for valid JSON
Cause: JSON contains fields not in struct
// JSON: {"name": "alice", "extra": "field"}
type User struct {
Name string `json:"name"`
}
user, err := binding.JSON[User](data, binding.WithStrictJSON())
// Error: json: unknown field "extra"
Solutions:
Add field to struct
type User struct {
Name string `json:"name"`
Extra string `json:"extra"`
}
Remove strict mode
user, err := binding.JSON[User](data) // Ignores extra fields
Use interface{} for unknown fields
type User struct {
Name string `json:"name"`
Extra map[string]interface{} `json:"-"`
}
Pointer vs Value Confusion
Problem: Can’t distinguish between “not provided” and “zero value”
Example:
type UpdateRequest struct {
Age int `json:"age"`
}
// JSON: {"age": 0}
// Can't tell if: 1) User wants to set age to 0, or 2) Field not provided
Solution: Use pointers
type UpdateRequest struct {
Age *int `json:"age"`
}
// JSON: {"age": 0} -> Age = &0 (explicitly set to zero)
// JSON: {} -> Age = nil (not provided)
// JSON: {"age": null} -> Age = nil (explicitly null)
Default Values Not Applied
Problem: Default value doesn’t work
Cause: Defaults only apply when field is missing, not for zero values
type Params struct {
Page int `query:"page" default:"1"`
}
// URL: ?page=0
params, _ := binding.Query[Params](values)
// Result: Page = 0 (not 1, because 0 was provided)
Solution: Use pointer to distinguish nil from zero
type Params struct {
Page *int `query:"page" default:"1"`
}
// URL: ?page=0 -> Page = &0
// URL: (no page) -> Page = &1 (default applied)
Nested Struct Not Binding
Problem: Nested struct fields remain zero
Example:
// JSON: {"user": {"name": "alice", "age": 30}}
type Request struct {
User struct {
Name string `json:"name"`
Age int `json:"age"`
} `json:"user"`
}
req, err := binding.JSON[Request](data)
// Works correctly
For query parameters, use dot notation:
// URL: ?user.name=alice&user.age=30
type Request struct {
User struct {
Name string `query:"user.name"`
Age int `query:"user.age"`
}
}
Time Parsing Errors
Problem: “parsing time … as …: cannot parse”
Cause: Time format doesn’t match any default layouts
// JSON: {"created": "01/02/2006"}
type Request struct {
Created time.Time `json:"created"`
}
// Error: parsing time "01/02/2006"
Solution: Add custom time layout
binder := binding.MustNew(
binding.WithTimeLayouts(
append(binding.DefaultTimeLayouts, "01/02/2006")...,
),
)
req, err := binder.JSON[Request](data)
Memory Issues
Problem: Out of memory or slow performance
Causes:
Large payloads without limits
// No limit - vulnerable to memory attack
user, err := binding.JSON[User](r.Body)
Solution: Set size limits
user, err := binding.JSON[User](r.Body,
binding.WithMaxBytes(1024*1024), // 1MB limit
binding.WithMaxSliceLen(1000),
binding.WithMaxMapSize(500),
)
Not using streaming for large data
// Bad - loads entire body into memory
body, _ := io.ReadAll(r.Body)
user, err := binding.JSON[User](body)
Solution: Stream from reader
user, err := binding.JSONReader[User](r.Body)
Problem: Header not binding
Cause: HTTP headers are case-insensitive but tag must match exact case
// Header: x-api-key: secret
type Request struct {
APIKey string `header:"X-API-Key"` // Still works!
}
// Headers are matched case-insensitively
Note: The binding package handles case-insensitive header matching automatically.
Multi-Source Precedence Issues
Problem: Wrong source value used
Example:
// Query: ?user_id=1
// JSON: {"user_id": 2}
type Request struct {
UserID int `query:"user_id" json:"user_id"`
}
req, err := binding.Bind[Request](
binding.FromQuery(values), // user_id = 1
binding.FromJSON(body), // user_id = 2 (overwrites!)
)
// Result: UserID = 2
Solutions:
Change source order (last wins)
req, err := binding.Bind[Request](
binding.FromJSON(body), // user_id = 2
binding.FromQuery(values), // user_id = 1 (overwrites!)
)
// Result: UserID = 1
Use first-wins strategy
req, err := binding.Bind[Request](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.FromQuery(values), // user_id = 1 (wins!)
binding.FromJSON(body), // user_id = 2 (ignored)
)
// Result: UserID = 1
Frequently Asked Questions
Q: How do I validate required fields?
A: Use the rivaas.dev/validation package after binding:
import "rivaas.dev/validation"
type Request struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"required,min=18"`
}
req, err := binding.JSON[Request](data)
if err != nil {
return err
}
// Validate after binding
if err := validation.Validate(req); err != nil {
return err
}
Q: Can I bind to non-struct types?
A: Yes, but only for certain types:
// Array
type Batch []CreateUserRequest
batch, err := binding.JSON[Batch](data)
// Map
type Config map[string]string
config, err := binding.JSON[Config](data)
// Primitive (less useful)
var count int
err := binding.JSONTo([]byte("42"), &count)
Q: How do I handle optional vs. required fields?
A: Combine binding with validation:
type Request struct {
Name string `json:"name" validate:"required"`
Email *string `json:"email" validate:"omitempty,email"`
}
// Name is required (validation)
// Email is optional (pointer) but if provided must be valid (validation)
Q: Can I use custom JSON field names?
A: Yes, use the json tag:
type User struct {
ID int `json:"user_id"` // Maps to "user_id" in JSON
FullName string `json:"full_name"` // Maps to "full_name" in JSON
}
Q: How do I bind from multiple query parameters to one field?
A: Use tag aliases:
type Request struct {
UserID int `query:"user_id,id,uid"` // Accepts any of these
}
// Works with: ?user_id=123, ?id=123, or ?uid=123
A: Yes, use multi-source binding:
type Request struct {
Name string `json:"name" form:"name"`
}
req, err := binding.Bind[Request](
binding.FromJSON(r.Body),
binding.FromForm(r.Form),
)
Q: How do I debug binding issues?
A: Use event hooks:
binder := binding.MustNew(
binding.WithEvents(binding.Events{
FieldBound: func(name, tag string) {
log.Printf("Bound %s from %s", name, tag)
},
UnknownField: func(name string) {
log.Printf("Unknown field: %s", name)
},
Done: func(stats binding.Stats) {
log.Printf("%d fields, %d errors, %v",
stats.FieldsBound, stats.ErrorCount, stats.Duration)
},
}),
)
Q: Is binding thread-safe?
A: Yes, all operations are thread-safe. The struct cache uses lock-free reads and synchronized writes.
Q: How do I bind custom types?
A: Register a converter:
import "github.com/google/uuid"
binder := binding.MustNew(
binding.WithConverter[uuid.UUID](uuid.Parse),
)
Or implement encoding.TextUnmarshaler:
type MyType string
func (m *MyType) UnmarshalText(text []byte) error {
*m = MyType(string(text))
return nil
}
Q: Can I bind from environment variables?
A: Not directly, but you can create a custom getter:
type EnvGetter struct{}
func (g *EnvGetter) Get(key string) string {
return os.Getenv(key)
}
func (g *EnvGetter) GetAll(key string) []string {
if val := os.Getenv(key); val != "" {
return []string{val}
}
return nil
}
func (g *EnvGetter) Has(key string) bool {
_, exists := os.LookupEnv(key)
return exists
}
// Use with RawInto
config, err := binding.RawInto[Config](&EnvGetter{}, "env")
Q: What’s the difference between JSON and JSONReader?
A:
JSON: Takes []byte, entire data in memoryJSONReader: Takes io.Reader, streams data
Use JSONReader for large payloads (>1MB) to reduce memory usage.
Q: How do I handle API versioning?
A: Use different struct types per version:
type CreateUserRequestV1 struct {
Name string `json:"name"`
}
type CreateUserRequestV2 struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// Route to appropriate handler based on version header
Debugging Strategies
1. Enable Debug Logging
import "log/slog"
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
2. Inspect Raw Request
// Save body for debugging
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(body))
log.Printf("Raw body: %s", string(body))
log.Printf("Content-Type: %s", r.Header.Get("Content-Type"))
req, err := binding.JSON[Request](r.Body)
3. Use Curl to Test
# Test JSON binding
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"alice","age":30}'
# Test query parameters
curl "http://localhost:8080/users?page=2&limit=50"
# Test headers
curl -H "X-API-Key: secret" http://localhost:8080/users
4. Write Unit Tests
func TestBinding(t *testing.T) {
payload := `{"name":"test","age":30}`
user, err := binding.JSON[User]([]byte(payload))
if err != nil {
t.Fatalf("binding failed: %v", err)
}
if user.Name != "test" {
t.Errorf("expected name=test, got %s", user.Name)
}
}
Getting Help
If you’re still stuck:
- Check the examples: Binding Guide
- Review API docs: API Reference
- Search GitHub issues: rivaas-dev/rivaas/issues
- Ask for help: Open a new issue with:
- Minimal reproducible example
- Expected vs. actual behavior
- Relevant logs/errors
See Also
For more examples and patterns, see the Binding Guide.