This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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.

Package Information

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:#d4edda

Quick Navigation

API Documentation

Performance and Troubleshooting

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

CategoryTypes
Primitivesstring, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool
Timetime.Time, time.Duration
Networknet.IP, net.IPNet, url.URL
Regexregexp.Regexp
Collections[]T, map[string]T
Pointers*T for any supported type
NestedNested 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),
)

Struct Tags

Control binding behavior with struct tags:

TagPurposeExample
jsonJSON body fieldjson:"field_name"
queryQuery parameterquery:"param_name"
formForm dataform:"field_name"
headerHTTP headerheader:"X-Header-Name"
pathPath parameterpath:"param_name"
cookieHTTP cookiecookie:"cookie_name"
defaultDefault valuedefault:"value"
validateValidation rulesvalidate:"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:

PackageFormatImport Path
yamlYAMLrivaas.dev/binding/yaml
tomlTOMLrivaas.dev/binding/toml
msgpackMessagePackrivaas.dev/binding/msgpack
protoProtocol Buffersrivaas.dev/binding/proto

Performance Characteristics

  • 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

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 parse
  • opts: 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())

Form

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)
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) error
  • FormTo(values url.Values, target interface{}, opts ...Option) error
  • HeaderTo(headers http.Header, target interface{}, opts ...Option) error
  • CookieTo(cookies []*http.Cookie, target interface{}, opts ...Option) error
  • PathTo(params map[string]string, target interface{}, opts ...Option) error
  • XMLTo(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 unmarshaling
  • json.Unmarshaler: For custom JSON unmarshaling
  • xml.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:

  1. Function-level options override binder-level options
  2. 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())

4. Collect All Errors for Forms

// 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-PackageFormatImport Path
yamlYAMLrivaas.dev/binding/yaml
tomlTOMLrivaas.dev/binding/toml
msgpackMessagePackrivaas.dev/binding/msgpack
protoProtocol Buffersrivaas.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

func WithStrict() Option

Enables strict YAML parsing. Fails on unknown fields or duplicate keys.

Example:

config, err := yaml.YAML[Config](data, yaml.WithStrict())

Struct Tags

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.

Struct Tags

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.

Struct Tags

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...
}

Multi-Format API

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:

PackageDependency
yamlgopkg.in/yaml.v3
tomlgithub.com/BurntSushi/toml
msgpackgithub.com/vmihailenco/msgpack/v5
protogoogle.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

Performance Comparison

Approximate performance for a typical struct (10 fields):

FormatSpeed (ns/op)AllocsUse Case
JSON8003Web APIs, human-readable
MessagePack5002High performance, binary
Protocol Buffers4002Strongly typed, cross-language
YAML1,2005Configuration files
TOML1,0004Configuration files

Best Practices

1. Use Appropriate Format

  • 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

2. Validate Input

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:

  1. Field is unexported

    // Wrong - unexported field
    type Request struct {
        name string `json:"name"`  // Won't bind
    }
    
    // Correct
    type Request struct {
        Name string `json:"name"`
    }
    
  2. 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
    }
    
  3. 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"`
    }
    
  4. 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:

  1. 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}

  2. Use string type and convert manually

    type User struct {
        AgeStr string `json:"age"`
    }
    
    user, err := binding.JSON[User](data)
    age, _ := strconv.Atoi(user.AgeStr)
    
  3. 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:

  1. Malformed JSON

    {"name": "test"  // Missing closing brace
    

    Solution: Validate JSON syntax

  2. 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)
    
  3. 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:

  1. Add field to struct

    type User struct {
        Name  string `json:"name"`
        Extra string `json:"extra"`
    }
    
  2. Remove strict mode

    user, err := binding.JSON[User](data)  // Ignores extra fields
    
  3. 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:

  1. 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),
    )
    
  2. 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)
    

Header Case Sensitivity

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:

  1. 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
    
  2. 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

Q: Can I use both JSON and form binding?

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 memory
  • JSONReader: 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:

  1. Check the examples: Binding Guide
  2. Review API docs: API Reference
  3. Search GitHub issues: rivaas-dev/rivaas/issues
  4. 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.