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

Return to the regular view of this page.

Request Data Binding

Learn how to bind HTTP request data to Go structs with type safety and performance

High-performance request data binding for Go web applications. Maps values from various sources (query parameters, form data, JSON bodies, headers, cookies, path parameters) into Go structs using struct tags.

Features

  • Multiple Sources - Query, path, form, header, cookie, JSON, XML, YAML, TOML, MessagePack, Protocol Buffers
  • Type Safe - Generic API for compile-time type safety
  • Zero Allocation - Struct reflection info cached for performance
  • Flexible - Nested structs, slices, maps, pointers, custom types
  • Error Context - Detailed field-level error information
  • Extensible - Custom type converters and value getters

Note: For validation (required fields, enum constraints, etc.), use the rivaas.dev/validation package separately after binding.

Quick Start

JSON Binding

import "rivaas.dev/binding"

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

// Generic API (preferred)
user, err := binding.JSON[CreateUserRequest](body)
if err != nil {
    // Handle error
}

Query Parameters

type ListParams struct {
    Page   int      `query:"page" default:"1"`
    Limit  int      `query:"limit" default:"20"`
    Tags   []string `query:"tags"`
    SortBy string   `query:"sort_by"`
}

params, err := binding.Query[ListParams](r.URL.Query())

Multi-Source Binding

Combine data from multiple sources:

type CreateOrderRequest struct {
    // From path parameters
    UserID int `path:"user_id"`
    
    // From query string
    Coupon string `query:"coupon"`
    
    // From headers
    Auth string `header:"Authorization"`
    
    // From JSON body
    Items []OrderItem `json:"items"`
    Total float64     `json:"total"`
}

req, err := binding.Bind[CreateOrderRequest](
    binding.FromPath(pathParams),
    binding.FromQuery(r.URL.Query()),
    binding.FromHeader(r.Header),
    binding.FromJSON(body),
)

Learning Path

Follow these guides to master request data binding with Rivaas:

  1. Installation - Get started with the binding package
  2. Basic Usage - Learn the fundamentals of binding data
  3. Query Parameters - Work with URL query strings
  4. JSON Binding - Handle JSON request bodies
  5. Multi-Source - Combine data from multiple sources
  6. Struct Tags - Master struct tag syntax and options
  7. Type Support - Built-in and custom type conversion
  8. Error Handling - Handle binding errors gracefully
  9. Advanced Usage - Custom getters, streaming, and more
  10. Examples - Real-world integration patterns

Supported Sources

SourceFunctionDescription
QueryQuery[T]()URL query parameters (?name=value)
PathPath[T]()URL path parameters (/users/:id)
FormForm[T]()Form data (application/x-www-form-urlencoded)
HeaderHeader[T]()HTTP headers
CookieCookie[T]()HTTP cookies
JSONJSON[T]()JSON body
XMLXML[T]()XML body
YAMLyaml.YAML[T]()YAML body (sub-package)
TOMLtoml.TOML[T]()TOML body (sub-package)
MessagePackmsgpack.MsgPack[T]()MessagePack body (sub-package)
Protocol Buffersproto.Proto[T]()Protobuf body (sub-package)

Why Generic API?

The binding package uses Go generics for compile-time type safety:

// Generic API (preferred) - Type-safe at compile time
user, err := binding.JSON[CreateUserRequest](body)

// Non-generic API - When type comes from variable
var user CreateUserRequest
err := binding.JSONTo(body, &user)

Benefits:

  • ✅ Compile-time type checking
  • ✅ No reflection overhead for type instantiation
  • ✅ Better IDE autocomplete
  • ✅ Cleaner, more readable code

Performance

  • First binding of a type: ~500ns overhead for reflection
  • Subsequent bindings: ~50ns overhead (cache lookup)
  • Query/Path/Form: Zero allocations for primitive types
  • Struct reflection info cached automatically

Next Steps

For integration with rivaas/app, the Context provides a convenient Bind() method that handles all the complexity automatically.

1 - Installation

Install and set up the Rivaas binding package for your Go application

Get started with the Rivaas binding package by installing it in your Go project.

Prerequisites

  • Go 1.25 or higher - The binding package requires Go 1.25+
  • Basic familiarity with Go generics

Installation

Install the binding package using go get:

go get rivaas.dev/binding

This will add the package to your go.mod file and download the dependencies.

Sub-Packages

The binding package includes optional sub-packages for additional format support:

YAML

go get rivaas.dev/binding/yaml

TOML

go get rivaas.dev/binding/toml

MessagePack

go get rivaas.dev/binding/msgpack

Protocol Buffers

go get rivaas.dev/binding/proto

Verify Installation

Create a simple test to verify the installation is working:

package main

import (
    "fmt"
    "net/url"
    "rivaas.dev/binding"
)

type TestParams struct {
    Name string `query:"name"`
    Age  int    `query:"age"`
}

func main() {
    values := url.Values{
        "name": []string{"Alice"},
        "age":  []string{"30"},
    }
    
    params, err := binding.Query[TestParams](values)
    if err != nil {
        panic(err)
    }
    
    fmt.Printf("Binding package installed successfully!\n")
    fmt.Printf("Name: %s, Age: %d\n", params.Name, params.Age)
}

Save this as main.go and run:

go run main.go

If you see the success message with parsed values, the installation is complete!

Import Paths

Always import the binding package using:

import "rivaas.dev/binding"

For sub-packages:

import (
    "rivaas.dev/binding"
    "rivaas.dev/binding/yaml"
    "rivaas.dev/binding/toml"
)

Common Issues

Go Version Too Old

If you get an error about Go version:

go: rivaas.dev/binding requires go >= 1.25

Update your Go installation to version 1.25 or higher:

go version  # Check current version

Visit golang.org/dl/ to download the latest version.

Module Not Found

If you get a “module not found” error:

go clean -modcache
go get rivaas.dev/binding

Dependency Conflicts

If you experience dependency conflicts, ensure your go.mod is up to date:

go mod tidy

Next Steps

Now that you have the binding package installed:

For complete API documentation, visit the API Reference.

2 - Basic Usage

Learn the fundamentals of binding request data to Go structs

This guide covers the essential operations for working with the binding package: binding from different sources, understanding the API variants, and handling errors.

Generic API vs Non-Generic API

The binding package provides two API styles:

Use the generic API when you know the type at compile time:

// Type is specified as a type parameter
user, err := binding.JSON[CreateUserRequest](body)
params, err := binding.Query[ListParams](r.URL.Query())

Benefits:

  • Compile-time type safety
  • Cleaner syntax
  • Better IDE support
  • No need to pre-allocate the struct

Non-Generic API

Use the non-generic API when the type comes from a variable or when working with interfaces:

var user CreateUserRequest
err := binding.JSONTo(body, &user)

var params ListParams
err := binding.QueryTo(r.URL.Query(), &params)

Use when:

  • Type is determined at runtime
  • Working with reflection
  • Integrating with older codebases

Binding from Different Sources

JSON Body

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

// Read body from request
body, err := io.ReadAll(r.Body)
if err != nil {
    // Handle error
}
defer r.Body.Close()

// Bind JSON to struct
user, err := binding.JSON[CreateUserRequest](body)
if err != nil {
    // Handle binding error
}

Query Parameters

type ListParams struct {
    Page   int      `query:"page" default:"1"`
    Limit  int      `query:"limit" default:"20"`
    Search string   `query:"search"`
    Tags   []string `query:"tags"`
}

params, err := binding.Query[ListParams](r.URL.Query())

Path Parameters

type UserIDParam struct {
    UserID int `path:"user_id"`
}

// Path params typically come from your router
// Example with common router pattern:
pathParams := map[string]string{
    "user_id": "123",
}

params, err := binding.Path[UserIDParam](pathParams)

Form Data

type LoginForm struct {
    Username string `form:"username"`
    Password string `form:"password"`
    Remember bool   `form:"remember"`
}

// Parse form first
if err := r.ParseForm(); err != nil {
    // Handle parse error
}

form, err := binding.Form[LoginForm](r.Form)

Headers

type RequestHeaders struct {
    Auth        string `header:"Authorization"`
    ContentType string `header:"Content-Type"`
    UserAgent   string `header:"User-Agent"`
}

headers, err := binding.Header[RequestHeaders](r.Header)

Cookies

type SessionCookies struct {
    SessionID string `cookie:"session_id"`
    CSRF      string `cookie:"csrf_token"`
}

cookies, err := binding.Cookie[SessionCookies](r.Cookies())

Error Handling Basics

All binding functions return an error that provides context about what went wrong:

user, err := binding.JSON[CreateUserRequest](body)
if err != nil {
    // Check for specific error types
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        fmt.Printf("Field %s: %v\n", bindErr.Field, bindErr.Err)
    }
    
    // Or just use the error message
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}

Common error types:

  • BindError - Field-level binding error with context
  • UnknownFieldError - Unknown fields in strict mode
  • MultiError - Multiple errors when using WithAllErrors()

See Error Handling for detailed information.

Default Values

Use the default tag to specify fallback values:

type Config struct {
    Port    int    `query:"port" default:"8080"`
    Host    string `query:"host" default:"localhost"`
    Debug   bool   `query:"debug" default:"false"`
    Timeout string `query:"timeout" default:"30s"`
}

// If query params don't include these values, defaults are used
cfg, err := binding.Query[Config](r.URL.Query())

Working with Pointers

Use pointers to distinguish between “not set” and “set to zero value”:

type UpdateUserRequest struct {
    Name  *string `json:"name"`   // nil = not updating, "" = clear value
    Email *string `json:"email"`
    Age   *int    `json:"age"`    // nil = not updating, 0 = set to zero
}

user, err := binding.JSON[UpdateUserRequest](body)

// Check if field was provided
if user.Name != nil {
    // Update name to *user.Name
}
if user.Age != nil {
    // Update age to *user.Age
}

Common Patterns

API Handler Pattern

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    // Read body
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read body", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()
    
    // Bind request
    req, err := binding.JSON[CreateUserRequest](body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Process request
    user := createUser(req)
    
    // Send response
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

Query + Path Parameters

type GetUserRequest struct {
    UserID int    `path:"user_id"`
    Format string `query:"format" default:"json"`
}

func GetUserHandler(w http.ResponseWriter, r *http.Request) {
    req, err := binding.Bind[GetUserRequest](
        binding.FromPath(pathParams),  // From router
        binding.FromQuery(r.URL.Query()),
    )
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    user := getUserByID(req.UserID)
    // Format response according to req.Format
}

Form with CSRF Token

type EditForm struct {
    Title   string `form:"title"`
    Content string `form:"content"`
    CSRF    string `form:"csrf_token"`
}

func EditHandler(w http.ResponseWriter, r *http.Request) {
    if err := r.ParseForm(); err != nil {
        http.Error(w, "Invalid form", http.StatusBadRequest)
        return
    }
    
    form, err := binding.Form[EditForm](r.Form)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Verify CSRF token
    if !verifyCRSF(form.CSRF) {
        http.Error(w, "Invalid CSRF token", http.StatusForbidden)
        return
    }
    
    // Process form
}

Type Conversion

The binding package automatically converts string values to appropriate types:

type Request struct {
    // String to int
    Page int `query:"page"`           // "123" -> 123
    
    // String to bool
    Active bool `query:"active"`      // "true" -> true
    
    // String to float
    Price float64 `query:"price"`     // "19.99" -> 19.99
    
    // String to time.Duration
    Timeout time.Duration `query:"timeout"`  // "30s" -> 30 * time.Second
    
    // String to time.Time
    CreatedAt time.Time `query:"created"`   // "2025-01-01" -> time.Time
    
    // String to slice
    Tags []string `query:"tags"`      // "go,rust,python" -> []string
}

See Type Support for complete type conversion details.

Performance Tips

  1. Reuse request bodies: Binding consumes the body, so read it once and reuse
  2. Use defaults: Struct tags with defaults avoid unnecessary error checking
  3. Cache reflection: Happens automatically, but avoid dynamic struct generation
  4. Stream large payloads: Use JSONReader for bodies > 1MB

Next Steps

For complete API documentation, see API Reference.

3 - Query Parameters

Master URL query string binding with slices, defaults, and type conversion

Learn how to bind URL query parameters to Go structs with automatic type conversion, default values, and slice handling.

Basic Query Binding

Query parameters are parsed from the URL query string:

// URL: /users?page=2&limit=50&search=john
type ListParams struct {
    Page   int    `query:"page"`
    Limit  int    `query:"limit"`
    Search string `query:"search"`
}

params, err := binding.Query[ListParams](r.URL.Query())
// Result: {Page: 2, Limit: 50, Search: "john"}

Default Values

Use the default tag to provide fallback values:

type PaginationParams struct {
    Page  int `query:"page" default:"1"`
    Limit int `query:"limit" default:"20"`
}

// URL: /items (no query params)
params, err := binding.Query[PaginationParams](r.URL.Query())
// Result: {Page: 1, Limit: 20}

// URL: /items?page=3
params, err := binding.Query[PaginationParams](r.URL.Query())
// Result: {Page: 3, Limit: 20}

Slice Handling

The binding package supports two modes for parsing slices:

Repeated Parameters (Default)

type FilterParams struct {
    Tags []string `query:"tags"`
}

// URL: /items?tags=go&tags=rust&tags=python
params, err := binding.Query[FilterParams](r.URL.Query())
// Result: {Tags: ["go", "rust", "python"]}

CSV Mode

Use WithSliceMode for comma-separated values:

// URL: /items?tags=go,rust,python
params, err := binding.Query[FilterParams](
    r.URL.Query(),
    binding.WithSliceMode(binding.SliceCSV),
)
// Result: {Tags: ["go", "rust", "python"]}

Type Conversion

Query parameters are automatically converted to appropriate types:

type QueryParams struct {
    // String to integer
    Age int `query:"age"`                    // "30" -> 30
    
    // String to boolean
    Active bool `query:"active"`             // "true" -> true
    
    // String to float
    Price float64 `query:"price"`            // "19.99" -> 19.99
    
    // String to time.Duration
    Timeout time.Duration `query:"timeout"`  // "30s" -> 30 * time.Second
    
    // String to time.Time
    Since time.Time `query:"since"`          // "2025-01-01" -> time.Time
    
    // String slice
    IDs []int `query:"ids"`                  // "1&2&3" -> [1, 2, 3]
}

Nested Structures

Use dot notation for nested structs:

type SearchParams struct {
    Query string `query:"q"`
    Filter struct {
        Category string `query:"category"`
        MinPrice int    `query:"min_price"`
        MaxPrice int    `query:"max_price"`
    } `query:"filter"`  // Prefix tag on parent struct
}

// URL: /search?q=laptop&filter.category=electronics&filter.min_price=500
params, err := binding.Query[SearchParams](r.URL.Query())

Tag Aliases

Support multiple parameter names for the same field:

type UserParams struct {
    UserID int `query:"user_id,id,uid"`  // Accepts any of these names
}

// All of these work:
// /users?user_id=123
// /users?id=123
// /users?uid=123

Optional Fields with Pointers

Use pointers to distinguish between “not provided” and “zero value”:

type OptionalParams struct {
    Limit  *int    `query:"limit"`   // nil if not provided
    Offset *int    `query:"offset"`  // nil if not provided
    Filter *string `query:"filter"`  // nil if not provided
}

// URL: /items?limit=10
params, err := binding.Query[OptionalParams](r.URL.Query())
// Result: {Limit: &10, Offset: nil, Filter: nil}

if params.Limit != nil {
    // Use *params.Limit
}

Complex Example

type ComplexSearchParams struct {
    // Basic fields
    Query string `query:"q"`
    Page  int    `query:"page" default:"1"`
    Limit int    `query:"limit" default:"20"`
    
    // Sorting
    SortBy    string `query:"sort_by" default:"created_at"`
    SortOrder string `query:"sort_order" default:"desc"`
    
    // Filters
    Tags       []string  `query:"tags"`
    Categories []string  `query:"categories"`
    MinPrice   *float64  `query:"min_price"`
    MaxPrice   *float64  `query:"max_price"`
    
    // Date range
    Since *time.Time `query:"since"`
    Until *time.Time `query:"until"`
    
    // Flags
    IncludeArchived bool `query:"include_archived"`
    IncludeDrafts   bool `query:"include_drafts"`
}

// URL: /search?q=laptop&tags=electronics&tags=sale&min_price=500&page=2
params, err := binding.Query[ComplexSearchParams](r.URL.Query())

Boolean Parsing

Boolean values accept multiple formats:

type Flags struct {
    Debug bool `query:"debug"`
}

// All of these parse to true:
// ?debug=true
// ?debug=1
// ?debug=yes
// ?debug=on

// All of these parse to false:
// ?debug=false
// ?debug=0
// ?debug=no
// ?debug=off
// (parameter not present)

Common Patterns

Pagination

type PaginationParams struct {
    Page     int `query:"page" default:"1"`
    PageSize int `query:"page_size" default:"20"`
}

func ListHandler(w http.ResponseWriter, r *http.Request) {
    params, err := binding.Query[PaginationParams](r.URL.Query())
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    offset := (params.Page - 1) * params.PageSize
    items := getItems(offset, params.PageSize)
    
    json.NewEncoder(w).Encode(items)
}

Search and Filter

type SearchParams struct {
    Q          string   `query:"q"`
    Categories []string `query:"category"`
    Tags       []string `query:"tag"`
    Sort       string   `query:"sort" default:"relevance"`
}

func SearchHandler(w http.ResponseWriter, r *http.Request) {
    params, err := binding.Query[SearchParams](r.URL.Query())
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    results := search(params.Q, params.Categories, params.Tags, params.Sort)
    json.NewEncoder(w).Encode(results)
}

Date Range Filtering

type DateRangeParams struct {
    StartDate time.Time `query:"start_date"`
    EndDate   time.Time `query:"end_date"`
}

// URL: /reports?start_date=2025-01-01&end_date=2025-12-31
params, err := binding.Query[DateRangeParams](r.URL.Query())

Error Handling

params, err := binding.Query[SearchParams](r.URL.Query())
if err != nil {
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        // Field-specific error
        log.Printf("Invalid query param %s: %v", bindErr.Field, bindErr.Err)
    }
    
    http.Error(w, "Invalid query parameters", http.StatusBadRequest)
    return
}

Validation

Note: The binding package focuses on type conversion. For validation (required fields, value ranges, etc.), use rivaas.dev/validation after binding:

params, err := binding.Query[SearchParams](r.URL.Query())
if err != nil {
    return err
}

// Validate after binding
if err := validation.Validate(params); err != nil {
    return err
}

Performance Tips

  1. Use defaults: Avoids checking for zero values
  2. Avoid reflection: Struct info is cached automatically
  3. Reuse structs: Define parameter structs once
  4. Primitive types: Zero allocation for basic types

Troubleshooting

Query Parameter Not Binding

Check that:

  • Tag name matches query parameter name
  • Field is exported (starts with uppercase)
  • Type conversion is supported
// Wrong - unexported field
type Params struct {
    page int `query:"page"`  // Won't bind
}

// Correct
type Params struct {
    Page int `query:"page"`
}

Slice Not Parsing

Ensure you’re using the correct slice mode:

// For repeated params: ?tags=go&tags=rust
params, err := binding.Query[Params](values)  // Default mode

// For CSV: ?tags=go,rust,python
params, err := binding.Query[Params](
    values,
    binding.WithSliceMode(binding.SliceCSV),
)

Next Steps

For complete API details, see API Reference.

4 - JSON Binding

Bind and parse JSON request bodies with automatic type conversion and validation

Learn how to bind JSON request bodies to Go structs with proper error handling, nested objects, and integration with validators.

Basic JSON Binding

Bind JSON request bodies directly to structs:

type CreateUserRequest struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Age      int    `json:"age"`
}

req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}

// Use req.Username, req.Email, req.Age

JSON Tags

The binding package respects standard json tags:

type Product struct {
    ID          int       `json:"id"`
    Name        string    `json:"name"`
    Price       float64   `json:"price"`
    CreatedAt   time.Time `json:"created_at"`
    
    // Omit if empty
    Description string    `json:"description,omitempty"`
    
    // Ignore this field
    Internal    string    `json:"-"`
}

Nested Structures

Handle complex nested JSON:

type Order struct {
    ID       string    `json:"id"`
    Customer struct {
        Name    string `json:"name"`
        Email   string `json:"email"`
        Address struct {
            Street  string `json:"street"`
            City    string `json:"city"`
            Country string `json:"country"`
            ZipCode string `json:"zip_code"`
        } `json:"address"`
    } `json:"customer"`
    Items []struct {
        ProductID string  `json:"product_id"`
        Quantity  int     `json:"quantity"`
        Price     float64 `json:"price"`
    } `json:"items"`
    Total float64 `json:"total"`
}

// POST /orders
// {
//   "id": "ORD-12345",
//   "customer": {
//     "name": "John Doe",
//     "email": "john@example.com",
//     "address": {
//       "street": "123 Main St",
//       "city": "New York",
//       "country": "USA",
//       "zip_code": "10001"
//     }
//   },
//   "items": [
//     {"product_id": "PROD-1", "quantity": 2, "price": 29.99}
//   ],
//   "total": 59.98
// }

order, err := binding.JSON[Order](r.Body)

Type Support

JSON binding supports rich type conversion:

type ComplexTypes struct {
    // Basic types
    String  string  `json:"string"`
    Int     int     `json:"int"`
    Float   float64 `json:"float"`
    Bool    bool    `json:"bool"`
    
    // Time types
    Timestamp time.Time     `json:"timestamp"`
    Duration  time.Duration `json:"duration"`
    
    // Slices
    Tags      []string `json:"tags"`
    Numbers   []int    `json:"numbers"`
    
    // Maps
    Metadata  map[string]string      `json:"metadata"`
    Settings  map[string]interface{} `json:"settings"`
    
    // Pointers (nullable)
    Optional  *string `json:"optional"`
    Nullable  *int    `json:"nullable"`
}

// Example JSON:
// {
//   "string": "hello",
//   "int": 42,
//   "float": 3.14,
//   "bool": true,
//   "timestamp": "2025-01-01T00:00:00Z",
//   "duration": "30s",
//   "tags": ["go", "rust"],
//   "numbers": [1, 2, 3],
//   "metadata": {"key": "value"},
//   "optional": null,
//   "nullable": 10
// }

Reading Limits

Protect against large payloads with WithMaxBytes:

// Limit to 1MB
req, err := binding.JSON[CreateUserRequest](
    r.Body,
    binding.WithMaxBytes(1024 * 1024),
)
if err != nil {
    http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
    return
}

Strict JSON Parsing

Reject unknown fields with WithDisallowUnknownFields:

type StrictRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// This will error if JSON contains fields not in the struct
req, err := binding.JSON[StrictRequest](
    r.Body,
    binding.WithDisallowUnknownFields(),
)

Optional Fields

Use pointers to distinguish between “not provided” and “zero value”:

type UpdateUserRequest struct {
    Username *string `json:"username,omitempty"`
    Email    *string `json:"email,omitempty"`
    Age      *int    `json:"age,omitempty"`
}

// JSON: {"email": "new@example.com"}
req, err := binding.JSON[UpdateUserRequest](r.Body)
// Result: {Username: nil, Email: &"new@example.com", Age: nil}

if req.Email != nil {
    // Update email to *req.Email
}

Array Bodies

Bind arrays directly:

type BatchRequest []struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

// JSON: [{"id": "1", "name": "A"}, {"id": "2", "name": "B"}]
batch, err := binding.JSON[BatchRequest](r.Body)

Complete HTTP Handler Example

func CreateProductHandler(w http.ResponseWriter, r *http.Request) {
    type CreateProductRequest struct {
        Name        string   `json:"name"`
        Description string   `json:"description"`
        Price       float64  `json:"price"`
        Categories  []string `json:"categories"`
        Stock       int      `json:"stock"`
    }
    
    // 1. Bind JSON
    req, err := binding.JSON[CreateProductRequest](
        r.Body,
        binding.WithMaxBytes(1024*1024),
        binding.WithDisallowUnknownFields(),
    )
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // 2. Validate (using rivaas.dev/validation)
    if err := validation.Validate(req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // 3. Business logic
    product := createProduct(req)
    
    // 4. Response
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(product)
}

Error Handling

The binding package provides detailed error information:

req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        // Field-specific error
        log.Printf("Failed to bind field %s: %v", bindErr.Field, bindErr.Err)
        http.Error(w, 
            fmt.Sprintf("Invalid field: %s", bindErr.Field),
            http.StatusBadRequest)
        return
    }
    
    // Generic error (malformed JSON, etc.)
    http.Error(w, "Invalid JSON", http.StatusBadRequest)
    return
}

Common Error Types

// Syntax errors
// {"name": "test"  <- missing closing brace
// Error: "unexpected end of JSON input"

// Type mismatch
// {"age": "not a number"}  <- age is int
// Error: "cannot unmarshal string into field age of type int"

// Unknown fields (with WithDisallowUnknownFields)
// {"name": "test", "unknown": "value"}
// Error: "json: unknown field \"unknown\""

// Request too large (with WithMaxBytes)
// Payload > limit
// Error: "http: request body too large"

Integration with Validation

Combine with rivaas.dev/validation for comprehensive validation:

import (
    "rivaas.dev/binding"
    "rivaas.dev/validation"
)

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,min=3,max=32"`
    Email    string `json:"email" validate:"required,email"`
    Age      int    `json:"age" validate:"required,min=18,max=120"`
}

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    // Step 1: Bind JSON structure
    req, err := binding.JSON[CreateUserRequest](r.Body)
    if err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    // Step 2: Validate business rules
    if err := validation.Validate(req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Proceed with valid data
    createUser(req)
}

Custom JSON Parsing

For special cases, implement json.Unmarshaler:

type Duration time.Duration

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    
    parsed, err := time.ParseDuration(s)
    if err != nil {
        return err
    }
    
    *d = Duration(parsed)
    return nil
}

type Config struct {
    Timeout Duration `json:"timeout"`
}

// JSON: {"timeout": "30s"}
cfg, err := binding.JSON[Config](r.Body)

Handling Multiple Content Types

Use binding.Auto() to handle both JSON and form data:

// Works with both:
// Content-Type: application/json
// Content-Type: application/x-www-form-urlencoded

req, err := binding.Auto[CreateUserRequest](r)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}

Performance Considerations

  1. Use io.LimitReader: Always set max bytes for untrusted input
  2. Avoid reflection: Type info is cached automatically
  3. Reuse structs: Define request types once
  4. Pointer fields: Only when you need to distinguish nil from zero

Best Practices

1. Separate Request/Response Types

// Request
type CreateUserRequest struct {
    Username string `json:"username"`
    Email    string `json:"email"`
}

// Response
type CreateUserResponse struct {
    ID       string `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
    Created  time.Time `json:"created"`
}

2. Use Validation Tags

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
    Email    string `json:"email" validate:"required,email"`
}

3. Document with Examples

// CreateUserRequest represents a new user creation request.
//
// Example JSON:
//
//	{
//	  "username": "johndoe",
//	  "email": "john@example.com",
//	  "age": 30
//	}
type CreateUserRequest struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Age      int    `json:"age"`
}

4. Set Limits

const maxRequestSize = 1024 * 1024 // 1MB

req, err := binding.JSON[CreateUserRequest](
    r.Body,
    binding.WithMaxBytes(maxRequestSize),
)

Testing

func TestCreateUserHandler(t *testing.T) {
    payload := `{"username": "test", "email": "test@example.com", "age": 25}`
    
    req := httptest.NewRequest("POST", "/users", strings.NewReader(payload))
    req.Header.Set("Content-Type", "application/json")
    
    rec := httptest.NewRecorder()
    CreateUserHandler(rec, req)
    
    if rec.Code != http.StatusCreated {
        t.Errorf("expected status 201, got %d", rec.Code)
    }
}

Next Steps

For complete API details, see API Reference.

5 - Multi-Source Binding

Combine multiple data sources with precedence rules for flexible request handling

Learn how to bind data from multiple sources (query parameters, JSON body, headers, etc.) with configurable precedence rules.

Concept Overview

Multi-source binding allows you to populate a single struct from multiple request sources, with clear precedence rules:

graph LR
    A[HTTP Request] --> B[Query Params]
    A --> C[JSON Body]
    A --> D[Headers]
    A --> E[Path Params]
    
    B --> F[Multi-Source Binder]
    C --> F
    D --> F
    E --> F
    
    F --> G[Merged Struct]
    
    style F fill:#e1f5ff
    style G fill:#d4edda

Basic Multi-Source Binding

Use binding.Auto() to bind from query, body, and headers automatically:

type UserRequest struct {
    // From query or JSON body
    Username string `json:"username" query:"username"`
    Email    string `json:"email" query:"email"`
    
    // From header
    APIKey   string `header:"X-API-Key"`
}

// Works with:
// - POST /users?username=john with JSON body
// - GET /users?username=john&email=john@example.com
// - Headers: X-API-Key: secret123

req, err := binding.Auto[UserRequest](r)

Custom Multi-Source

Build custom multi-source binding with explicit precedence:

type SearchRequest struct {
    Query    string   `query:"q" json:"query"`
    Page     int      `query:"page" default:"1"`
    PageSize int      `query:"page_size" default:"20"`
    Filters  []string `json:"filters"`
    SortBy   string   `header:"X-Sort-By" default:"created_at"`
}

// Bind from multiple sources
req, err := binding.Multi[SearchRequest](
    binding.WithQuery(r.URL.Query()),
    binding.WithJSON(r.Body),
    binding.WithHeaders(r.Header),
)

Precedence Rules

By default, sources are applied in order (last wins):

// Example: User ID from multiple sources
type Request struct {
    UserID int `query:"user_id" json:"user_id" header:"X-User-ID"`
}

// Query: ?user_id=1
// JSON:  {"user_id": 2}
// Header: X-User-ID: 3

// Default precedence (last wins):
req, err := binding.Multi[Request](
    binding.WithQuery(r.URL.Query()),      // user_id = 1
    binding.WithJSON(r.Body),              // user_id = 2 (overwrites)
    binding.WithHeaders(r.Header),         // user_id = 3 (overwrites)
)
// Result: user_id = 3

First-Wins Precedence

Use WithMergeStrategy to prefer first non-empty value:

req, err := binding.Multi[Request](
    binding.WithMergeStrategy(binding.MergeFirstWins),
    binding.WithQuery(r.URL.Query()),      // user_id = 1
    binding.WithJSON(r.Body),              // user_id = 2 (ignored)
    binding.WithHeaders(r.Header),         // user_id = 3 (ignored)
)
// Result: user_id = 1

Partial Binding

Different fields can come from different sources:

type CompleteRequest struct {
    // Pagination from query
    Page     int `query:"page" default:"1"`
    PageSize int `query:"page_size" default:"20"`
    
    // Search criteria from JSON body
    Filters struct {
        Category string   `json:"category"`
        Tags     []string `json:"tags"`
        MinPrice float64  `json:"min_price"`
        MaxPrice float64  `json:"max_price"`
    } `json:"filters"`
    
    // Auth from headers
    APIKey    string `header:"X-API-Key"`
    RequestID string `header:"X-Request-ID"`
}

// POST /search?page=2&page_size=50
// Headers: X-API-Key: secret, X-Request-ID: req-123
// Body: {"filters": {"category": "electronics", "tags": ["sale"]}}

req, err := binding.Multi[CompleteRequest](
    binding.WithQuery(r.URL.Query()),
    binding.WithJSON(r.Body),
    binding.WithHeaders(r.Header),
)

Path Parameters

Combine with router path parameters:

type UserUpdateRequest struct {
    // From path: /users/:id
    UserID int `path:"id"`
    
    // From JSON body
    Username string `json:"username"`
    Email    string `json:"email"`
    
    // From header
    APIKey string `header:"X-API-Key"`
}

// With gorilla/mux or chi
req, err := binding.Multi[UserUpdateRequest](
    binding.WithPath(mux.Vars(r)),  // or chi.URLParams(r)
    binding.WithJSON(r.Body),
    binding.WithHeaders(r.Header),
)

Form Data and JSON

Handle both form and JSON submissions:

type LoginRequest struct {
    Username string `json:"username" form:"username"`
    Password string `json:"password" form:"password"`
}

// Works with both:
// Content-Type: application/json
// Content-Type: application/x-www-form-urlencoded

req, err := binding.Auto[LoginRequest](r)

Source-Specific Options

Apply options to specific sources:

req, err := binding.Multi[Request](
    binding.WithQuery(r.URL.Query()),
    
    binding.WithJSON(r.Body,
        binding.WithMaxBytes(1024*1024),
        binding.WithDisallowUnknownFields(),
    ),
    
    binding.WithHeaders(r.Header),
)

Conditional Sources

Bind from sources based on conditions:

func BindRequest[T any](r *http.Request) (T, error) {
    sources := []binding.Source{
        binding.WithQuery(r.URL.Query()),
    }
    
    // Add JSON source only for POST/PUT/PATCH
    if r.Method != "GET" && r.Method != "DELETE" {
        sources = append(sources, binding.WithJSON(r.Body))
    }
    
    // Add auth header if present
    if r.Header.Get("Authorization") != "" {
        sources = append(sources, binding.WithHeaders(r.Header))
    }
    
    return binding.Multi[T](sources...)
}

Complex Example

Real-world multi-source scenario:

type ProductSearchRequest struct {
    // Query parameters (user input)
    Query    string   `query:"q"`
    Page     int      `query:"page" default:"1"`
    PageSize int      `query:"page_size" default:"20"`
    SortBy   string   `query:"sort_by" default:"relevance"`
    
    // Advanced filters (JSON body)
    Filters struct {
        Categories []string `json:"categories"`
        Brands     []string `json:"brands"`
        MinPrice   float64  `json:"min_price"`
        MaxPrice   float64  `json:"max_price"`
        InStock    *bool    `json:"in_stock"`
        Rating     *int     `json:"min_rating"`
    } `json:"filters"`
    
    // Request metadata (headers)
    Locale      string `header:"Accept-Language" default:"en-US"`
    Currency    string `header:"X-Currency" default:"USD"`
    UserAgent   string `header:"User-Agent"`
    RequestID   string `header:"X-Request-ID"`
    
    // Internal fields (not from request)
    UserID      int    `binding:"-"`  // Set after auth
    RequestedAt time.Time `binding:"-"`
}

func SearchProducts(w http.ResponseWriter, r *http.Request) {
    // Bind from multiple sources
    req, err := binding.Multi[ProductSearchRequest](
        binding.WithQuery(r.URL.Query()),
        binding.WithJSON(r.Body, binding.WithMaxBytes(1024*1024)),
        binding.WithHeaders(r.Header),
    )
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Set internal fields
    req.UserID = getUserID(r)
    req.RequestedAt = time.Now()
    
    // Execute search
    results := executeSearch(req)
    json.NewEncoder(w).Encode(results)
}

Error Handling

Multi-source errors include source information:

req, err := binding.Multi[Request](
    binding.WithQuery(r.URL.Query()),
    binding.WithJSON(r.Body),
)
if err != nil {
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        log.Printf("Source: %s, Field: %s, Error: %v",
            bindErr.Source, bindErr.Field, bindErr.Err)
    }
    
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}

Source Priority Pattern

Common pattern for API versioning and backward compatibility:

type VersionedRequest struct {
    // Prefer header, fallback to query
    APIVersion string `header:"X-API-Version" query:"api_version" default:"v1"`
    
    // Prefer body, fallback to query
    UserID int `json:"user_id" query:"user_id"`
}

// With first-wins strategy:
req, err := binding.Multi[VersionedRequest](
    binding.WithMergeStrategy(binding.MergeFirstWins),
    binding.WithHeaders(r.Header),   // Highest priority
    binding.WithQuery(r.URL.Query()), // Fallback
    binding.WithJSON(r.Body),         // Lowest priority
)

Middleware Pattern

Create reusable binding middleware:

func BindMiddleware[T any](next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        req, err := binding.Multi[T](
            binding.WithQuery(r.URL.Query()),
            binding.WithJSON(r.Body),
            binding.WithHeaders(r.Header),
        )
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        
        // Store in context
        ctx := context.WithValue(r.Context(), "request", req)
        next(w, r.WithContext(ctx))
    }
}

// Usage
http.HandleFunc("/users", BindMiddleware[CreateUserRequest](CreateUserHandler))

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    req := r.Context().Value("request").(CreateUserRequest)
    // Use req
}

Integration with Rivaas Router

Seamless integration with rivaas.dev/router:

import (
    "rivaas.dev/binding"
    "rivaas.dev/router"
)

type CreateUserRequest struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    APIKey   string `header:"X-API-Key"`
}

r := router.New()

r.POST("/users", func(c *router.Context) error {
    req, err := binding.Multi[CreateUserRequest](
        binding.WithJSON(c.Request().Body),
        binding.WithHeaders(c.Request().Header),
    )
    if err != nil {
        return c.JSON(http.StatusBadRequest, err)
    }
    
    // Use req
    return c.JSON(http.StatusCreated, createUser(req))
})

Performance Considerations

  1. Source order: Most specific first (headers before query)
  2. Lazy evaluation: Sources are processed in order
  3. Caching: Struct info is cached across requests
  4. Zero allocation: Primitive types use no extra memory

Best Practices

1. Document Source Expectations

// SearchRequest accepts search parameters from multiple sources:
//   - Query: pagination (page, page_size)
//   - JSON body: filters (categories, price range)
//   - Headers: locale, currency
type SearchRequest struct {
    // ...
}

2. Use Defaults Wisely

type Request struct {
    Page int `query:"page" default:"1"`     // Good
    Sort string `header:"X-Sort" query:"sort" default:"created_at"`  // Good
}

3. Validate After Binding

req, err := binding.Multi[Request](...)
if err != nil {
    return err
}

// Validate business rules
if err := validation.Validate(req); err != nil {
    return err
}

Troubleshooting

Values Not Merging

Check tag names match across sources:

// Wrong - different tag names
type Request struct {
    ID int `query:"id" json:"user_id"`  // Won't merge
}

// Correct - same semantic field
type Request struct {
    ID int `query:"id" json:"id"`
}

Unexpected Overwrites

Use first-wins strategy or check source order:

// Last wins (default)
binding.Multi[T](
    binding.WithQuery(...),  // Applied first
    binding.WithJSON(...),   // May overwrite query
)

// First wins (explicit)
binding.Multi[T](
    binding.WithMergeStrategy(binding.MergeFirstWins),
    binding.WithHeaders(...),  // Highest priority
    binding.WithQuery(...),
)

Next Steps

For complete API details, see API Reference.

6 - Struct Tags

Master struct tag syntax for precise control over data binding

Comprehensive guide to struct tag syntax, options, and conventions for the binding package.

Overview

Struct tags control how fields are bound from different sources. The binding package supports multiple tag types:

type Example struct {
    Field string `json:"field" query:"field" header:"X-Field" default:"value"`
}

Tag Types

Source Tags

TagSourceExample
jsonJSON bodyjson:"field_name"
queryURL query paramsquery:"field_name"
formForm dataform:"field_name"
headerHTTP headersheader:"X-Field-Name"
pathURL path paramspath:"param_name"
cookieHTTP cookiescookie:"cookie_name"

Special Tags

TagPurposeExample
defaultDefault valuedefault:"value"
validateValidation rulesvalidate:"required,email"
bindingControl bindingbinding:"-" or binding:"required"

Basic Syntax

Simple Field

type User struct {
    Name string `json:"name"`
}

Multiple Sources

Same field can bind from multiple sources:

type Request struct {
    UserID int `query:"user_id" json:"user_id" header:"X-User-ID"`
}

Field Name Mapping

Map different source names to same field:

type Request struct {
    UserID int `query:"uid" json:"user_id" header:"X-User-ID"`
}

JSON Tags

Standard encoding/json tag syntax:

type Product struct {
    // Basic field
    ID int `json:"id"`
    
    // Custom name
    Name string `json:"product_name"`
    
    // Omit if empty
    Description string `json:"description,omitempty"`
    
    // Ignore field
    Internal string `json:"-"`
    
    // Use field name as-is (case-sensitive)
    SKU string `json:"SKU"`
}

JSON Tag Options

type Example struct {
    // Omit if empty/zero value
    Optional string `json:"optional,omitempty"`
    
    // Omit if empty AND keep format
    Field string `json:"field,omitempty,string"`
    
    // Treat as string (for numbers)
    ID int64 `json:"id,string"`
}

Query Tags

URL query parameter binding:

type QueryParams struct {
    // Basic parameter
    Search string `query:"q"`
    
    // With default
    Page int `query:"page" default:"1"`
    
    // Array/slice
    Tags []string `query:"tags"`
    
    // Optional with pointer
    Filter *string `query:"filter"`
}

Query Tag Aliases

Support multiple parameter names:

type Request struct {
    // Accepts any of: user_id, id, uid
    UserID int `query:"user_id,id,uid"`
}

Header Tags

HTTP header binding:

type HeaderParams struct {
    // Standard header
    ContentType string `header:"Content-Type"`
    
    // Custom header
    APIKey string `header:"X-API-Key"`
    
    // Case-insensitive
    UserAgent string `header:"user-agent"`  // Matches User-Agent
    
    // Authorization
    AuthToken string `header:"Authorization"`
}

Header Naming Conventions

Headers are case-insensitive:

type Example struct {
    // All match "X-API-Key", "x-api-key", "X-Api-Key"
    APIKey string `header:"X-API-Key"`
}

Path Tags

URL path parameter binding:

// Route: /users/:id
type PathParams struct {
    UserID int `path:"id"`
}

// Route: /posts/:category/:slug
type PostParams struct {
    Category string `path:"category"`
    Slug     string `path:"slug"`
}

Form Tags

Form data binding:

type FormData struct {
    Username string `form:"username"`
    Email    string `form:"email"`
    Age      int    `form:"age"`
}

HTTP cookie binding:

type CookieParams struct {
    SessionID string `cookie:"session_id"`
    Theme     string `cookie:"theme" default:"light"`
}

Default Tag

Specify default values for fields:

type Config struct {
    // String default
    Host string `query:"host" default:"localhost"`
    
    // Integer default
    Port int `query:"port" default:"8080"`
    
    // Boolean default
    Debug bool `query:"debug" default:"false"`
    
    // Duration default
    Timeout time.Duration `query:"timeout" default:"30s"`
}

Default Value Types

type Defaults struct {
    String   string        `default:"text"`
    Int      int           `default:"42"`
    Float    float64       `default:"3.14"`
    Bool     bool          `default:"true"`
    Duration time.Duration `default:"1h30m"`
    Time     time.Time     `default:"2025-01-01T00:00:00Z"`
}

Binding Tag

Control binding behavior:

type Request struct {
    // Skip binding entirely
    Internal string `binding:"-"`
    
    // Required field
    UserID int `binding:"required"`
    
    // Optional field (explicit)
    Email string `binding:"optional"`
}

Validation Tag

Integration with rivaas.dev/validation:

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
    Email    string `json:"email" validate:"required,email"`
    Age      int    `json:"age" validate:"required,min=18,max=120"`
    Website  string `json:"website" validate:"omitempty,url"`
}

Common Validation Rules

type ValidationExamples struct {
    // Required
    Required string `validate:"required"`
    
    // Length constraints
    Username string `validate:"min=3,max=32"`
    
    // Format validation
    Email    string `validate:"email"`
    URL      string `validate:"url"`
    UUID     string `validate:"uuid"`
    
    // Numeric constraints
    Age      int     `validate:"min=18,max=120"`
    Price    float64 `validate:"gt=0"`
    
    // Pattern matching
    Phone    string `validate:"regexp=^[0-9]{10}$"`
    
    // Conditional
    Optional string `validate:"omitempty,email"`  // Validate only if present
}

Tag Combinations

Complete Example

type CompleteRequest struct {
    // Multi-source with default and validation
    UserID int `query:"user_id" json:"user_id" header:"X-User-ID" default:"0" validate:"min=1"`
    
    // Optional with validation
    Email string `json:"email" validate:"omitempty,email"`
    
    // Required with custom name
    APIKey string `header:"X-API-Key" binding:"required"`
    
    // Array with default
    Tags []string `query:"tags" default:"general"`
    
    // Nested struct
    Filters struct {
        Category string `json:"category" validate:"required"`
        MinPrice int    `json:"min_price" validate:"min=0"`
    } `json:"filters"`
}

Embedded Structs

Tags on embedded structs:

type Pagination struct {
    Page     int `query:"page" default:"1"`
    PageSize int `query:"page_size" default:"20"`
}

type SearchRequest struct {
    Query string `query:"q"`
    Pagination  // Embedded - inherits tags
}

// Usage
req, err := binding.Query[SearchRequest](values)
// Can access req.Page, req.PageSize

Embedded with Prefix

type SearchRequest struct {
    Query      string `query:"q"`
    Pagination `query:"pagination"`  // Adds prefix
}

// URL: ?q=test&pagination.page=2&pagination.page_size=50

Pointer Fields

Pointers distinguish “not provided” from “zero value”:

type UpdateRequest struct {
    // nil = not provided, &0 = set to zero
    Age *int `json:"age"`
    
    // nil = not provided, &"" = set to empty string
    Bio *string `json:"bio"`
    
    // nil = not provided, &false = set to false
    Active *bool `json:"active"`
}

Tag Naming Conventions

JSON (snake_case)

type User struct {
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
    EmailAddr string `json:"email_address"`
}

Query (snake_case or kebab-case)

type Params struct {
    UserID   int `query:"user_id"`
    SortBy   string `query:"sort_by"`
    SortOrder string `query:"sort-order"`  // kebab-case also fine
}

Headers (Title-Case)

type Headers struct {
    ContentType string `header:"Content-Type"`
    APIKey      string `header:"X-API-Key"`
    RequestID   string `header:"X-Request-ID"`
}

Ignored Fields

Multiple ways to ignore fields:

type Example struct {
    // Unexported - automatically ignored
    internal string
    
    // Explicitly ignored with json tag
    Debug string `json:"-"`
    
    // Explicitly ignored with binding tag
    Temporary string `binding:"-"`
    
    // Exported but not bound
    Computed int  // No tags
}

Complex Types

Time Fields

type TimeFields struct {
    // RFC3339 format
    CreatedAt time.Time `json:"created_at"`
    
    // Unix timestamp (as integer)
    UpdatedAt time.Time `json:"updated_at,unix"`
    
    // Duration
    Timeout time.Duration `json:"timeout"`  // "30s", "1h", etc.
}

Map Fields

type Config struct {
    // String map
    Metadata map[string]string `json:"metadata"`
    
    // Nested map
    Settings map[string]interface{} `json:"settings"`
    
    // Typed map
    Counters map[string]int `json:"counters"`
}

Interface Fields

type Flexible struct {
    // Any JSON value
    Data interface{} `json:"data"`
    
    // Strongly typed when possible
    Config map[string]interface{} `json:"config"`
}

Tag Best Practices

1. Be Consistent

// Good - consistent naming
type User struct {
    UserID    int    `json:"user_id"`
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
}

// Bad - inconsistent naming
type User struct {
    UserID    int    `json:"userId"`
    FirstName string `json:"first_name"`
    LastName  string `json:"LastName"`
}

2. Use Defaults for Common Values

type Pagination struct {
    Page     int `query:"page" default:"1"`
    PageSize int `query:"page_size" default:"20"`
}

3. Validate After Binding

// Separate binding from validation
type Request struct {
    Email string `json:"email" validate:"required,email"`
}

// Bind first
req, err := binding.JSON[Request](r.Body)
// Then validate
err = validation.Validate(req)

4. Document Complex Tags

// UserRequest represents a user creation request.
// The user_id can come from query, JSON, or X-User-ID header.
// If not provided, defaults to 0 (anonymous user).
type UserRequest struct {
    UserID int `query:"user_id" json:"user_id" header:"X-User-ID" default:"0"`
}

Tag Parsing Rules

  1. Tag precedence: Last source wins (unless using first-wins strategy)
  2. Case sensitivity:
    • JSON: case-sensitive
    • Query: case-sensitive
    • Headers: case-insensitive
  3. Empty values: Use omitempty to skip
  4. Type conversion: Automatic for supported types
  5. Validation: Applied after binding

Common Patterns

API Versioning

type VersionedRequest struct {
    APIVersion string `header:"X-API-Version" query:"api_version" default:"v1"`
    Data       interface{} `json:"data"`
}

Tenant Isolation

type TenantRequest struct {
    TenantID string `header:"X-Tenant-ID" binding:"required"`
    Data     interface{} `json:"data"`
}

Audit Fields

type AuditableRequest struct {
    RequestID string    `header:"X-Request-ID"`
    UserAgent string    `header:"User-Agent"`
    ClientIP  string    `header:"X-Forwarded-For"`
    Timestamp time.Time `binding:"-"`  // Set by server
}

Troubleshooting

Field Not Binding

Check that:

  1. Field is exported (starts with uppercase)
  2. Tag name matches source key
  3. Tag type matches source (e.g., query for query params)
// Wrong
type Bad struct {
    name string `json:"name"`  // Unexported
}

// Correct
type Good struct {
    Name string `json:"name"`
}

Type Conversion Failing

Ensure source data matches field type:

// URL: ?age=twenty
type Params struct {
    Age int `query:"age"`  // Will error - can't convert "twenty" to int
}

Default Not Applied

Defaults only apply when field is missing, not for zero values:

type Params struct {
    Page int `query:"page" default:"1"`
}

// ?page=0 -> Page = 0 (not 1, zero was provided)
// (no page param) -> Page = 1 (default applied)

Next Steps

For complete API details, see API Reference.

7 - Type Support

Complete reference for all supported data types and conversions

Comprehensive guide to type support in the binding package, including automatic conversions, custom types, and edge cases.

Supported Types

The binding package supports a wide range of Go types with automatic conversion:

Basic Types

type BasicTypes struct {
    // String
    Name string `json:"name"`
    
    // Integers
    Int    int    `json:"int"`
    Int8   int8   `json:"int8"`
    Int16  int16  `json:"int16"`
    Int32  int32  `json:"int32"`
    Int64  int64  `json:"int64"`
    
    // Unsigned integers
    Uint   uint   `json:"uint"`
    Uint8  uint8  `json:"uint8"`
    Uint16 uint16 `json:"uint16"`
    Uint32 uint32 `json:"uint32"`
    Uint64 uint64 `json:"uint64"`
    
    // Floats
    Float32 float32 `json:"float32"`
    Float64 float64 `json:"float64"`
    
    // Boolean
    Active bool `json:"active"`
    
    // Byte (alias for uint8)
    Byte byte `json:"byte"`
    
    // Rune (alias for int32)
    Rune rune `json:"rune"`
}

String Conversions

From Query/Header

type StringParams struct {
    Name  string `query:"name"`
    Value string `header:"X-Value"`
}

// URL: ?name=John+Doe
// Header: X-Value: hello world
// Result: {Name: "John Doe", Value: "hello world"}

From JSON

type JSONStrings struct {
    Text string `json:"text"`
}

// JSON: {"text": "hello"}
// Result: {Text: "hello"}

Empty Strings

type Optional struct {
    // Empty string is valid
    Name string `json:"name"`  // "" is kept
    
    // Use pointer for "not provided"
    Bio *string `json:"bio"`  // nil if not in JSON
}

Integer Conversions

From Strings

type Numbers struct {
    Age   int   `query:"age"`
    Count int64 `header:"X-Count"`
}

// URL: ?age=30
// Header: X-Count: 1000000
// Result: {Age: 30, Count: 1000000}

From JSON

type JSONNumbers struct {
    ID    int   `json:"id"`
    Count int64 `json:"count"`
}

// JSON: {"id": 42, "count": 9223372036854775807}

Overflow Handling

type SmallInt struct {
    Value int8 `json:"value"`
}

// JSON: {"value": 200}
// Error: value 200 overflows int8 (max 127)

Float Conversions

From Strings

type Floats struct {
    Price  float64 `query:"price"`
    Rating float32 `query:"rating"`
}

// URL: ?price=19.99&rating=4.5
// Result: {Price: 19.99, Rating: 4.5}

Scientific Notation

// URL: ?value=1.23e10
// Result: Value = 12300000000.0

Special Values

type SpecialFloats struct {
    Value float64 `query:"value"`
}

// URL: ?value=inf  -> +Inf
// URL: ?value=-inf -> -Inf
// URL: ?value=nan  -> NaN

Boolean Conversions

True Values

type Flags struct {
    Debug bool `query:"debug"`
}

// All parse to true:
// ?debug=true
// ?debug=1
// ?debug=yes
// ?debug=on
// ?debug=t
// ?debug=y

False Values

// All parse to false:
// ?debug=false
// ?debug=0
// ?debug=no
// ?debug=off
// ?debug=f
// ?debug=n
// (parameter not present)

Case Insensitive

// All parse to true:
// ?debug=TRUE
// ?debug=True
// ?debug=tRuE

Time Types

time.Time

type TimeFields struct {
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `query:"updated_at"`
}

// Supported formats:
// - RFC3339: "2025-01-01T00:00:00Z"
// - RFC3339Nano: "2025-01-01T00:00:00.123456789Z"
// - Date only: "2025-01-01"
// - Unix timestamp: "1735689600"

time.Duration

type Timeouts struct {
    Timeout   time.Duration `json:"timeout"`
    RetryAfter time.Duration `query:"retry_after"`
}

// Supported formats:
// - "300ms" -> 300 milliseconds
// - "1.5s" -> 1.5 seconds
// - "2m30s" -> 2 minutes 30 seconds
// - "1h30m" -> 1 hour 30 minutes
// - "24h" -> 24 hours

// URL: ?retry_after=30s
// JSON: {"timeout": "5m"}
// Result: {Timeout: 5*time.Minute, RetryAfter: 30*time.Second}

Slices and Arrays

String Slices

type Lists struct {
    Tags []string `query:"tags"`
}

// Repeated parameters (default):
// ?tags=go&tags=rust&tags=python
// Result: {Tags: ["go", "rust", "python"]}

// CSV mode:
// ?tags=go,rust,python
params, err := binding.Query[Lists](
    values,
    binding.WithSliceMode(binding.SliceCSV),
)
// Result: {Tags: ["go", "rust", "python"]}

Integer Slices

type IDList struct {
    IDs []int `query:"ids"`
}

// URL: ?ids=1&ids=2&ids=3
// Result: {IDs: [1, 2, 3]}

Float Slices

type Prices struct {
    Values []float64 `json:"values"`
}

// JSON: {"values": [19.99, 29.99, 39.99]}

Arrays (Fixed Size)

type FixedArray struct {
    RGB [3]int `json:"rgb"`
}

// JSON: {"rgb": [255, 128, 0]}
// Result: {RGB: [255, 128, 0]}

// JSON: {"rgb": [255, 128]}
// Error: array length mismatch

Nested Slices

type Matrix struct {
    Grid [][]int `json:"grid"`
}

// JSON: {"grid": [[1,2,3], [4,5,6], [7,8,9]]}

Maps

String Maps

type StringMaps struct {
    Metadata map[string]string `json:"metadata"`
    Labels   map[string]string `json:"labels"`
}

// JSON: {"metadata": {"key1": "value1", "key2": "value2"}}

Typed Maps

type TypedMaps struct {
    Counters map[string]int     `json:"counters"`
    Prices   map[string]float64 `json:"prices"`
    Flags    map[string]bool    `json:"flags"`
}

// JSON: {
//   "counters": {"views": 100, "likes": 50},
//   "prices": {"basic": 9.99, "premium": 29.99},
//   "flags": {"enabled": true, "public": false}
// }

Interface Maps

type FlexibleMap struct {
    Settings map[string]interface{} `json:"settings"`
}

// JSON: {
//   "settings": {
//     "name": "app",
//     "port": 8080,
//     "debug": true,
//     "features": ["a", "b", "c"]
//   }
// }

Nested Maps

type NestedMaps struct {
    Config map[string]map[string]string `json:"config"`
}

// JSON: {
//   "config": {
//     "database": {"host": "localhost", "port": "5432"},
//     "cache": {"host": "localhost", "port": "6379"}
//   }
// }

Pointers

Basic Pointers

type Pointers struct {
    // nil = not provided, &0 = explicitly zero
    Age *int `json:"age"`
    
    // nil = not provided, &"" = explicitly empty
    Bio *string `json:"bio"`
    
    // nil = not provided, &false = explicitly false
    Active *bool `json:"active"`
}

// JSON: {"age": 0, "bio": "", "active": false}
// Result: {Age: &0, Bio: &"", Active: &false}

// JSON: {}
// Result: {Age: nil, Bio: nil, Active: nil}

Pointer Semantics

type Update struct {
    Name *string `json:"name"`
}

// Distinguish between:
// 1. Not updating: {"other_field": "value"}
//    -> Name = nil (don't update)
// 
// 2. Setting to empty: {"name": ""}
//    -> Name = &"" (update to empty)
// 
// 3. Setting value: {"name": "John"}
//    -> Name = &"John" (update to John)

Double Pointers

type DoublePointer struct {
    Value **int `json:"value"`
}

// Supported but rarely needed

Structs

Nested Structs

type Order struct {
    ID string `json:"id"`
    Customer struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    } `json:"customer"`
    Items []struct {
        ID    string  `json:"id"`
        Price float64 `json:"price"`
    } `json:"items"`
}

Embedded Structs

type Base struct {
    ID        int       `json:"id"`
    CreatedAt time.Time `json:"created_at"`
}

type User struct {
    Base  // Embedded - fields promoted
    Name  string `json:"name"`
    Email string `json:"email"`
}

// JSON: {"id": 1, "created_at": "2025-01-01T00:00:00Z", "name": "John"}

Anonymous Structs

type Response struct {
    Data struct {
        Message string `json:"message"`
        Code    int    `json:"code"`
    } `json:"data"`
}

Interfaces

Empty Interface

type Flexible struct {
    Data interface{} `json:"data"`
}

// JSON: {"data": "string"}  -> Data = "string"
// JSON: {"data": 42}        -> Data = float64(42)
// JSON: {"data": true}      -> Data = true
// JSON: {"data": [1,2,3]}   -> Data = []interface{}{1,2,3}
// JSON: {"data": {"k":"v"}} -> Data = map[string]interface{}{"k":"v"}

Type Assertions

func handleData(d interface{}) {
    switch v := d.(type) {
    case string:
        fmt.Println("String:", v)
    case float64:
        fmt.Println("Number:", v)
    case bool:
        fmt.Println("Boolean:", v)
    case []interface{}:
        fmt.Println("Array:", v)
    case map[string]interface{}:
        fmt.Println("Object:", v)
    }
}

Custom Types

Type Aliases

type UserID int
type Email string

type User struct {
    ID    UserID `json:"id"`
    Email Email  `json:"email"`
}

// Binds like underlying type
// JSON: {"id": 123, "email": "test@example.com"}

Custom Unmarshalers

Implement json.Unmarshaler for custom parsing:

type CustomDuration time.Duration

func (cd *CustomDuration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    
    d, err := time.ParseDuration(s)
    if err != nil {
        return err
    }
    
    *cd = CustomDuration(d)
    return nil
}

type Config struct {
    Timeout CustomDuration `json:"timeout"`
}

TextUnmarshaler

For query/header parsing:

type Status string

const (
    StatusActive   Status = "active"
    StatusInactive Status = "inactive"
)

func (s *Status) UnmarshalText(text []byte) error {
    str := string(text)
    switch str {
    case "active", "inactive":
        *s = Status(str)
        return nil
    default:
        return fmt.Errorf("invalid status: %s", str)
    }
}

type Params struct {
    Status Status `query:"status"`
}

// URL: ?status=active

Type Conversion Matrix

Source TypeTarget TypeConversionExample
stringintParse"42"42
stringfloat64Parse"3.14"3.14
stringboolParse"true"true
stringtime.DurationParse"30s"30*time.Second
stringtime.TimeParse"2025-01-01"time.Time
numberintCast42.042
numberstringFormat42"42"
boolstringFormattrue"true"
array[]TElement-wise[1,2,3][]int{1,2,3}
objectstructField-wise{"a":1}struct{A int}
objectmapKey-value{"a":1}map[string]int

Edge Cases

Null vs Zero

type Nullable struct {
    // Pointer distinguishes null from zero
    Count *int `json:"count"`
}

// JSON: {"count": null} -> Count = nil
// JSON: {"count": 0}    -> Count = &0
// JSON: {}              -> Count = nil

Empty vs Missing

type Optional struct {
    Name  string  `json:"name"`
    Email *string `json:"email"`
}

// JSON: {"name": "", "email": ""}
// Result: {Name: "", Email: &""}

// JSON: {"name": ""}
// Result: {Name: "", Email: nil}

Overflow Protection

// Protects against overflow
type SafeInt struct {
    Value int8 `json:"value"`
}

// JSON: {"value": 200}
// Error: value overflows int8

Type Mismatches

type Typed struct {
    Age int `json:"age"`
}

// JSON: {"age": "not a number"}
// Error: cannot unmarshal string into int

Performance Characteristics

TypeAllocationSpeedNotes
PrimitivesZeroFastDirect assignment
StringsOneFastImmutable
SlicesOneFastPre-allocated when possible
MapsOneMediumHash allocation
StructsZeroFastStack allocation
PointersOneFastHeap allocation
InterfacesOneMediumType assertion overhead

Unsupported Types

The following types are not supported:

type Unsupported struct {
    // Channel
    Ch chan int  // Not supported
    
    // Function
    Fn func()  // Not supported
    
    // Complex numbers
    C complex128  // Not supported
    
    // Unsafe pointer
    Ptr unsafe.Pointer  // Not supported
}

Best Practices

1. Use Appropriate Types

// Good - specific types
type Good struct {
    Age      int       `json:"age"`
    Price    float64   `json:"price"`
    Created  time.Time `json:"created"`
}

// Bad - generic types
type Bad struct {
    Age     interface{} `json:"age"`
    Price   interface{} `json:"price"`
    Created interface{} `json:"created"`
}

2. Use Pointers for Optional Fields

type Update struct {
    Name *string `json:"name"`  // Can be null
    Age  *int    `json:"age"`   // Can be null
}

3. Use Slices for Variable-Length Data

// Good - slice
type Good struct {
    Tags []string `json:"tags"`
}

// Bad - fixed array
type Bad struct {
    Tags [10]string `json:"tags"`  // Rigid
}

4. Document Custom Types

// UserID represents a unique user identifier.
// It must be a positive integer.
type UserID int

// Validate ensures the UserID is valid.
func (id UserID) Validate() error {
    if id <= 0 {
        return errors.New("invalid user ID")
    }
    return nil
}

Troubleshooting

Type Conversion Errors

// Error: cannot unmarshal string into int
// Solution: Check source data matches target type

// Error: value overflows int8
// Solution: Use larger type (int16, int32, int64)

// Error: parsing time "invalid" as "2006-01-02"
// Solution: Use correct time format

Unexpected Nil Values

// Problem: field is nil when expected
// Solution: Check if source provided the value

// Problem: can't distinguish nil from zero
// Solution: Use pointer type

Next Steps

For complete API documentation, see API Reference.

8 - Error Handling

Master error handling patterns for robust request validation and debugging

Comprehensive guide to error handling in the binding package, including error types, validation patterns, and debugging strategies.

Error Types

The binding package provides structured error types for detailed error handling:

// BindError represents a field-specific binding error
type BindError struct {
    Field  string // Field name that failed
    Source string // Source ("query", "json", "header", etc.)
    Err    error  // Underlying error
}

// ValidationError represents a validation failure
type ValidationError struct {
    Field   string // Field name that failed validation
    Value   interface{} // The invalid value
    Rule    string // Validation rule that failed
    Message string // Human-readable message
}

Basic Error Handling

Simple Pattern

user, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}

Detailed Pattern

user, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        // Field-specific error
        log.Printf("Failed to bind field %s from %s: %v",
            bindErr.Field, bindErr.Source, bindErr.Err)
    }
    
    http.Error(w, "Invalid request", http.StatusBadRequest)
    return
}

Common Error Patterns

Type Conversion Errors

type Params struct {
    Age int `query:"age"`
}

// URL: ?age=invalid
// Error: BindError{
//   Field: "Age",
//   Source: "query",
//   Err: strconv.NumError{...}
// }

Missing Required Fields

type Request struct {
    APIKey string `header:"X-API-Key" binding:"required"`
}

// Missing header
// Error: BindError{
//   Field: "APIKey",
//   Source: "header",
//   Err: errors.New("required field missing")
// }

JSON Syntax Errors

// Malformed JSON: {"name": "test"
// Error: json.SyntaxError{...}

// Unknown field (with WithDisallowUnknownFields)
// Error: json.UnmarshalTypeError{...}

Size Limit Errors

req, err := binding.JSON[Request](
    r.Body,
    binding.WithMaxBytes(1024*1024),
)

// Request > 1MB
// Error: http.MaxBytesError{...}

Error Response Patterns

Basic JSON Error

func handleError(w http.ResponseWriter, err error) {
    type ErrorResponse struct {
        Error string `json:"error"`
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusBadRequest)
    json.NewEncoder(w).Encode(ErrorResponse{
        Error: err.Error(),
    })
}

// Usage
req, err := binding.JSON[Request](r.Body)
if err != nil {
    handleError(w, err)
    return
}

Detailed Error Response

type DetailedErrorResponse struct {
    Error   string                 `json:"error"`
    Details []FieldError           `json:"details,omitempty"`
}

type FieldError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
    Code    string `json:"code,omitempty"`
}

func handleBindError(w http.ResponseWriter, err error) {
    response := DetailedErrorResponse{
        Error: "Invalid request",
    }
    
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        response.Details = []FieldError{
            {
                Field:   bindErr.Field,
                Message: bindErr.Err.Error(),
                Code:    "BIND_ERROR",
            },
        }
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusBadRequest)
    json.NewEncoder(w).Encode(response)
}

RFC 7807 Problem Details

type ProblemDetail struct {
    Type     string                 `json:"type"`
    Title    string                 `json:"title"`
    Status   int                    `json:"status"`
    Detail   string                 `json:"detail,omitempty"`
    Instance string                 `json:"instance,omitempty"`
    Errors   map[string]interface{} `json:"errors,omitempty"`
}

func problemDetail(r *http.Request, err error) ProblemDetail {
    pd := ProblemDetail{
        Type:     "https://api.example.com/problems/invalid-request",
        Title:    "Invalid Request",
        Status:   http.StatusBadRequest,
        Instance: r.URL.Path,
        Errors:   make(map[string]interface{}),
    }
    
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        pd.Errors[bindErr.Field] = bindErr.Err.Error()
        pd.Detail = fmt.Sprintf("Field '%s' is invalid", bindErr.Field)
    } else {
        pd.Detail = err.Error()
    }
    
    return pd
}

// Usage
req, err := binding.JSON[Request](r.Body)
if err != nil {
    pd := problemDetail(r, err)
    w.Header().Set("Content-Type", "application/problem+json")
    w.WriteHeader(pd.Status)
    json.NewEncoder(w).Encode(pd)
    return
}

Validation Integration

Combine binding with validation:

import (
    "rivaas.dev/binding"
    "rivaas.dev/validation"
)

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
    Email    string `json:"email" validate:"required,email"`
    Age      int    `json:"age" validate:"required,min=18,max=120"`
}

func CreateUser(w http.ResponseWriter, r *http.Request) {
    // Step 1: Bind request
    req, err := binding.JSON[CreateUserRequest](r.Body)
    if err != nil {
        handleBindError(w, err)
        return
    }
    
    // Step 2: Validate
    if err := validation.Validate(req); err != nil {
        handleValidationError(w, err)
        return
    }
    
    // Process valid request
    user := createUser(req)
    json.NewEncoder(w).Encode(user)
}

func handleValidationError(w http.ResponseWriter, err error) {
    var valErrs validation.Errors
    if errors.As(err, &valErrs) {
        response := DetailedErrorResponse{
            Error: "Validation failed",
        }
        
        for _, valErr := range valErrs {
            response.Details = append(response.Details, FieldError{
                Field:   valErr.Field,
                Message: valErr.Message,
                Code:    valErr.Rule,
            })
        }
        
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusUnprocessableEntity)
        json.NewEncoder(w).Encode(response)
        return
    }
    
    http.Error(w, err.Error(), http.StatusBadRequest)
}

Error Context

Add context to errors for better debugging:

func bindRequest[T any](r *http.Request) (T, error) {
    req, err := binding.JSON[T](r.Body)
    if err != nil {
        return req, fmt.Errorf("binding request from %s: %w", r.RemoteAddr, err)
    }
    return req, nil
}

Error Logging

Structured Logging

import "log/slog"

func handleRequest(w http.ResponseWriter, r *http.Request) {
    req, err := binding.JSON[Request](r.Body)
    if err != nil {
        var bindErr *binding.BindError
        if errors.As(err, &bindErr) {
            slog.Error("Binding error",
                "field", bindErr.Field,
                "source", bindErr.Source,
                "error", bindErr.Err,
                "path", r.URL.Path,
                "method", r.Method,
                "remote", r.RemoteAddr,
            )
        } else {
            slog.Error("Request binding failed",
                "error", err,
                "path", r.URL.Path,
                "method", r.Method,
            )
        }
        
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }
    
    // Process request
}

Error Metrics

import "rivaas.dev/metrics"

var (
    bindErrorsCounter = metrics.NewCounter(
        "binding_errors_total",
        "Total number of binding errors",
        "field", "source", "error_type",
    )
)

func handleBindError(err error) {
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        bindErrorsCounter.Inc(
            bindErr.Field,
            bindErr.Source,
            fmt.Sprintf("%T", bindErr.Err),
        )
    }
}

Multi-Error Handling

Handle multiple errors from multi-source binding:

type MultiError []error

func (me MultiError) Error() string {
    var msgs []string
    for _, err := range me {
        msgs = append(msgs, err.Error())
    }
    return strings.Join(msgs, "; ")
}

func handleMultiError(w http.ResponseWriter, err error) {
    var multiErr MultiError
    if errors.As(err, &multiErr) {
        response := DetailedErrorResponse{
            Error: "Multiple validation errors",
        }
        
        for _, e := range multiErr {
            var bindErr *binding.BindError
            if errors.As(e, &bindErr) {
                response.Details = append(response.Details, FieldError{
                    Field:   bindErr.Field,
                    Message: bindErr.Err.Error(),
                })
            }
        }
        
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(response)
        return
    }
    
    http.Error(w, err.Error(), http.StatusBadRequest)
}

Error Recovery

Graceful Degradation

func loadConfig(r *http.Request) Config {
    cfg, err := binding.Query[Config](r.URL.Query())
    if err != nil {
        // Log error but use defaults
        slog.Warn("Failed to bind config, using defaults", "error", err)
        return DefaultConfig()
    }
    return cfg
}

Partial Success

func processBatch(items []Item) ([]Result, []error) {
    var results []Result
    var errors []error
    
    for _, item := range items {
        result, err := binding.Unmarshal[ProcessedItem](item.Data)
        if err != nil {
            errors = append(errors, fmt.Errorf("item %s: %w", item.ID, err))
            continue
        }
        results = append(results, Result{ID: item.ID, Data: result})
    }
    
    return results, errors
}

Error Testing

Unit Tests

func TestBindingError(t *testing.T) {
    type Request struct {
        Age int `json:"age"`
    }
    
    // Test invalid type
    body := strings.NewReader(`{"age": "not a number"}`)
    _, err := binding.JSON[Request](body)
    
    if err == nil {
        t.Fatal("expected error, got nil")
    }
    
    var bindErr *binding.BindError
    if !errors.As(err, &bindErr) {
        t.Fatalf("expected BindError, got %T", err)
    }
    
    if bindErr.Field != "Age" {
        t.Errorf("expected field Age, got %s", bindErr.Field)
    }
}

Integration Tests

func TestErrorResponse(t *testing.T) {
    payload := `{"age": "invalid"}`
    req := httptest.NewRequest("POST", "/users", strings.NewReader(payload))
    req.Header.Set("Content-Type", "application/json")
    
    rec := httptest.NewRecorder()
    CreateUserHandler(rec, req)
    
    if rec.Code != http.StatusBadRequest {
        t.Errorf("expected status 400, got %d", rec.Code)
    }
    
    var response ErrorResponse
    if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
        t.Fatal(err)
    }
    
    if response.Error == "" {
        t.Error("expected error message")
    }
}

Best Practices

1. Always Check Errors

// Good
req, err := binding.JSON[Request](r.Body)
if err != nil {
    handleError(w, err)
    return
}

// Bad - ignoring errors
req, _ := binding.JSON[Request](r.Body)

2. Use Specific Error Types

// Good - check specific error types
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
    // Handle binding error specifically
}

// Bad - generic error handling
if err != nil {
    http.Error(w, "error", 500)
}

3. Log for Debugging

// Good - structured logging
slog.Error("Binding failed",
    "error", err,
    "path", r.URL.Path,
    "user", getUserID(r),
)

// Bad - no logging
if err != nil {
    http.Error(w, "error", 400)
    return
}

4. Return Helpful Messages

// Good - specific error message
type ErrorResponse struct {
    Error  string       `json:"error"`
    Field  string       `json:"field,omitempty"`
    Detail string       `json:"detail,omitempty"`
}

// Bad - generic message
http.Error(w, "bad request", 400)

5. Separate Binding from Validation

// Good - clear separation
req, err := binding.JSON[Request](r.Body)
if err != nil {
    return handleBindError(err)
}

if err := validation.Validate(req); err != nil {
    return handleValidationError(err)
}

// Bad - mixing concerns
if err := bindAndValidate(r.Body); err != nil {
    // Can't tell binding from validation errors
}

Error Middleware

Create reusable error handling middleware:

type ErrorHandler func(http.ResponseWriter, *http.Request) error

func (fn ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        handleError(w, r, err)
    }
}

func handleError(w http.ResponseWriter, r *http.Request, err error) {
    // Log error
    slog.Error("Request error",
        "error", err,
        "path", r.URL.Path,
        "method", r.Method,
    )
    
    // Determine status code
    status := http.StatusInternalServerError
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        status = http.StatusBadRequest
    }
    
    // Send response
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(map[string]string{
        "error": err.Error(),
    })
}

// Usage
http.Handle("/users", ErrorHandler(func(w http.ResponseWriter, r *http.Request) error {
    req, err := binding.JSON[CreateUserRequest](r.Body)
    if err != nil {
        return err
    }
    
    // Process request
    return nil
}))

Common Error Scenarios

Scenario 1: Type Mismatch

// Request: {"age": "twenty"}
// Expected: {"age": 20}
// Error: cannot unmarshal string into int

Solution: Validate input format, provide clear error message

Scenario 2: Missing Required Field

// Request: {}
// Expected: {"api_key": "secret"}
// Error: required field 'api_key' missing

Solution: Use binding:"required" tag or validation

Scenario 3: Invalid JSON

// Request: {"name": "test"
// Error: unexpected EOF

Solution: Check Content-Type header, validate JSON syntax

Scenario 4: Request Too Large

// Request: 10MB payload
// Limit: 1MB
// Error: http: request body too large

Solution: Set appropriate WithMaxBytes() limit

Debugging Tips

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))

slog.Debug("Raw request body", "body", string(body))

req, err := binding.JSON[Request](r.Body)

3. Check Headers

slog.Debug("Request headers",
    "content-type", r.Header.Get("Content-Type"),
    "content-length", r.Header.Get("Content-Length"),
)

4. Use Error Wrapping

if err != nil {
    return fmt.Errorf("processing request from %s: %w", r.RemoteAddr, err)
}

Next Steps

For complete error type documentation, see API Reference.

9 - Advanced Usage

Advanced techniques including custom converters, binders, and extension patterns

Explore advanced binding techniques for custom types, sources, and integration patterns.

Custom Type Converters

Register converters for types not natively supported:

import (
    "github.com/google/uuid"
    "github.com/shopspring/decimal"
    "rivaas.dev/binding"
)

binder := binding.MustNew(
    binding.WithConverter[uuid.UUID](uuid.Parse),
    binding.WithConverter[decimal.Decimal](decimal.NewFromString),
)

type Product struct {
    ID    uuid.UUID       `query:"id"`
    Price decimal.Decimal `query:"price"`
}

// URL: ?id=550e8400-e29b-41d4-a716-446655440000&price=19.99
product, err := binder.Query[Product](values)

Converter Function Signature

type ConverterFunc[T any] func(string) (T, error)

// Example: Custom email type
type Email string

func ParseEmail(s string) (Email, error) {
    if !strings.Contains(s, "@") {
        return "", errors.New("invalid email format")
    }
    return Email(s), nil
}

binder := binding.MustNew(
    binding.WithConverter[Email](ParseEmail),
)

Custom ValueGetter

Implement custom data sources:

// ValueGetter interface
type ValueGetter interface {
    Get(key string) string
    GetAll(key string) []string
    Has(key string) bool
}

// Example: Environment variables 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
}

// Usage
type Config struct {
    APIKey string `env:"API_KEY"`
    Port   int    `env:"PORT" default:"8080"`
}

getter := &EnvGetter{}
config, err := binding.RawInto[Config](getter, "env")

GetterFunc Adapter

Use a function as a ValueGetter:

getter := binding.GetterFunc(func(key string) ([]string, bool) {
    // Custom lookup logic
    if val, ok := customSource[key]; ok {
        return []string{val}, true
    }
    return nil, false
})

result, err := binding.Raw[MyStruct](getter, "custom")

Map-Based Getters

Convenience helpers for simple sources:

// Single-value map
data := map[string]string{"name": "Alice", "age": "30"}
getter := binding.MapGetter(data)
result, err := binding.RawInto[User](getter, "custom")

// Multi-value map (for slices)
multi := map[string][]string{
    "tags": {"go", "rust", "python"},
    "name": {"Alice"},
}
getter := binding.MultiMapGetter(multi)
result, err := binding.RawInto[User](getter, "custom")

Reusable Binders

Create configured binders for shared settings:

var AppBinder = binding.MustNew(
    // Type converters
    binding.WithConverter[uuid.UUID](uuid.Parse),
    binding.WithConverter[decimal.Decimal](decimal.NewFromString),
    
    // Time formats
    binding.WithTimeLayouts("2006-01-02", "01/02/2006"),
    
    // Security limits
    binding.WithMaxDepth(16),
    binding.WithMaxSliceLen(1000),
    binding.WithMaxMapSize(500),
    
    // Error handling
    binding.WithAllErrors(),
    
    // Observability
    binding.WithEvents(binding.Events{
        FieldBound: logFieldBound,
        UnknownField: logUnknownField,
        Done: logBindingStats,
    }),
)

// Use across handlers
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    user, err := AppBinder.JSON[CreateUserRequest](r.Body)
    if err != nil {
        handleError(w, err)
        return
    }
    // ...
}

Observability Hooks

Monitor binding operations:

binder := binding.MustNew(
    binding.WithEvents(binding.Events{
        // Called when a field is successfully bound
        FieldBound: func(name, tag string) {
            metrics.Increment("binding.field.bound", "field:"+name, "source:"+tag)
        },
        
        // Called when an unknown field is encountered
        UnknownField: func(name string) {
            slog.Warn("Unknown field in request", "field", name)
            metrics.Increment("binding.field.unknown", "field:"+name)
        },
        
        // Called after binding completes
        Done: func(stats binding.Stats) {
            slog.Info("Binding completed",
                "fields_bound", stats.FieldsBound,
                "errors", stats.ErrorCount,
                "duration", stats.Duration,
            )
            
            metrics.Histogram("binding.duration", stats.Duration.Milliseconds())
            metrics.Gauge("binding.fields.bound", stats.FieldsBound)
        },
    }),
)

Binding Stats

type Stats struct {
    FieldsBound int           // Number of fields successfully bound
    ErrorCount  int           // Number of errors encountered
    Duration    time.Duration // Time taken for binding
}

Custom Struct Tags

Extend binding with custom tag behavior:

// Example: Custom "env" tag handler
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
}

// Register custom tag handler
binder := binding.MustNew(
    binding.WithTagHandler("env", &EnvTagHandler{prefix: "APP_"}),
)

type Config struct {
    APIKey string `env:"API_KEY"`  // Looks up APP_API_KEY
    Port   int    `env:"PORT"`     // Looks up APP_PORT
}

Streaming for Large Payloads

Use Reader variants for efficient memory usage:

// Instead of reading entire body into memory:
// body, _ := io.ReadAll(r.Body)  // Bad for large payloads
// user, err := binding.JSON[User](body)

// Stream directly from reader:
user, err := binding.JSONReader[User](r.Body)  // Memory-efficient

// Also available for XML, YAML:
doc, err := binding.XMLReader[Document](r.Body)
config, err := yaml.YAMLReader[Config](r.Body)

Nested Struct Binding

Dot Notation for Query Parameters

type SearchRequest struct {
    Query string `query:"q"`
    Filter struct {
        Category string `query:"filter.category"`
        MinPrice int    `query:"filter.min_price"`
        MaxPrice int    `query:"filter.max_price"`
        Tags     []string `query:"filter.tags"`
    }
}

// URL: ?q=laptop&filter.category=electronics&filter.min_price=100
params, err := binding.Query[SearchRequest](values)

Embedded Structs

type Timestamps struct {
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Timestamps  // Embedded - fields promoted
}

// JSON: {
//   "id": 1,
//   "name": "Alice",
//   "created_at": "2025-01-01T00:00:00Z",
//   "updated_at": "2025-01-01T12:00:00Z"
// }

Multi-Source with Priority

Control precedence of multiple sources:

type Request struct {
    UserID int    `query:"user_id" json:"user_id" header:"X-User-ID"`
    Token  string `header:"Authorization" query:"token"`
}

// Last source wins (default)
req, err := binding.Bind[Request](
    binding.FromQuery(r.URL.Query()),  // Lowest priority
    binding.FromJSON(r.Body),          // Medium priority
    binding.FromHeader(r.Header),      // Highest priority
)

// First source wins (explicit)
req, err := binding.Bind[Request](
    binding.WithMergeStrategy(binding.MergeFirstWins),
    binding.FromHeader(r.Header),      // Highest priority
    binding.FromJSON(r.Body),          // Medium priority
    binding.FromQuery(r.URL.Query()),  // Lowest priority
)

Conditional Binding

Bind based on request properties:

func BindRequest[T any](r *http.Request) (T, error) {
    sources := []binding.Source{}
    
    // Always include query params
    sources = append(sources, binding.FromQuery(r.URL.Query()))
    
    // Include body only for certain methods
    if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
        contentType := r.Header.Get("Content-Type")
        
        switch {
        case strings.Contains(contentType, "application/json"):
            sources = append(sources, binding.FromJSON(r.Body))
        case strings.Contains(contentType, "application/x-www-form-urlencoded"):
            sources = append(sources, binding.FromForm(r.Body))
        case strings.Contains(contentType, "application/xml"):
            sources = append(sources, binding.FromXML(r.Body))
        }
    }
    
    // Always include headers
    sources = append(sources, binding.FromHeader(r.Header))
    
    return binding.Bind[T](sources...)
}

Partial Updates

Handle PATCH requests with optional fields:

type UpdateUserRequest struct {
    Name     *string `json:"name"`      // nil = don't update
    Email    *string `json:"email"`     // nil = don't update
    Age      *int    `json:"age"`       // nil = don't update
    Active   *bool   `json:"active"`    // nil = don't update
}

func UpdateUser(w http.ResponseWriter, r *http.Request) {
    update, err := binding.JSON[UpdateUserRequest](r.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Only update fields that were provided
    if update.Name != nil {
        user.Name = *update.Name
    }
    if update.Email != nil {
        user.Email = *update.Email
    }
    if update.Age != nil {
        user.Age = *update.Age
    }
    if update.Active != nil {
        user.Active = *update.Active
    }
    
    saveUser(user)
}

Middleware Integration

Generic Binding Middleware

func BindMiddleware[T any](next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        req, err := binding.JSON[T](r.Body)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        
        // Store in context
        ctx := context.WithValue(r.Context(), "request", req)
        next(w, r.WithContext(ctx))
    }
}

// Usage
http.HandleFunc("/users",
    BindMiddleware[CreateUserRequest](CreateUserHandler))

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    req := r.Context().Value("request").(CreateUserRequest)
    // Use req...
}

With Validation

func BindAndValidate[T any](next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        req, err := binding.JSON[T](r.Body)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        
        // Validate
        if err := validation.Validate(req); err != nil {
            http.Error(w, err.Error(), http.StatusUnprocessableEntity)
            return
        }
        
        ctx := context.WithValue(r.Context(), "request", req)
        next(w, r.WithContext(ctx))
    }
}

Batch Binding

Process multiple items with error collection:

type BatchRequest []CreateUserRequest

func ProcessBatch(w http.ResponseWriter, r *http.Request) {
    batch, err := binding.JSON[BatchRequest](r.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    results := make([]Result, len(batch))
    errors := make([]error, 0)
    
    for i, item := range batch {
        user, err := createUser(item)
        if err != nil {
            errors = append(errors, fmt.Errorf("item %d: %w", i, err))
            continue
        }
        results[i] = Result{Success: true, User: user}
    }
    
    response := BatchResponse{
        Results: results,
        Errors:  errors,
    }
    
    json.NewEncoder(w).Encode(response)
}

TextUnmarshaler Integration

Implement custom text unmarshaling:

type Status string

const (
    StatusActive   Status = "active"
    StatusInactive Status = "inactive"
    StatusPending  Status = "pending"
)

func (s *Status) UnmarshalText(text []byte) error {
    str := string(text)
    switch str {
    case "active", "inactive", "pending":
        *s = Status(str)
        return nil
    default:
        return fmt.Errorf("invalid status: %s", str)
    }
}

type User struct {
    ID     int    `json:"id"`
    Name   string `json:"name"`
    Status Status `json:"status"` // Automatically uses UnmarshalText
}

Performance Optimization

Pre-allocate Slices

type Response struct {
    Items []Item `json:"items"`
}

// With capacity hint
items := make([]Item, 0, expectedSize)
// Bind into pre-allocated slice

Reuse Buffers

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func bindWithPool(r io.Reader) (User, error) {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()
    
    io.Copy(buf, r)
    return binding.JSON[User](buf.Bytes())
}

Avoid Reflection in Hot Paths

// Cache binder instance
var binder = binding.MustNew(
    binding.WithConverter[uuid.UUID](uuid.Parse),
)

// Struct info is cached automatically after first use
// Subsequent bindings have minimal overhead

Testing Helpers

Mock Requests

func TestBindingJSON(t *testing.T) {
    payload := `{"name": "Alice", "age": 30}`
    body := io.NopCloser(strings.NewReader(payload))
    
    user, err := binding.JSON[User](body)
    if err != nil {
        t.Fatal(err)
    }
    
    if user.Name != "Alice" {
        t.Errorf("expected Alice, got %s", user.Name)
    }
}

Test Different Sources

func TestMultiSource(t *testing.T) {
    req, err := binding.Bind[Request](
        binding.FromQuery(url.Values{
            "page": []string{"1"},
        }),
        binding.FromJSON([]byte(`{"name":"test"}`)),
        binding.FromHeader(http.Header{
            "X-API-Key": []string{"secret"},
        }),
    )
    
    if err != nil {
        t.Fatal(err)
    }
    
    // Assertions...
}

Integration Patterns

With Rivaas Router

import "rivaas.dev/router"

r := router.New()

r.POST("/users", func(c *router.Context) error {
    user, err := binding.JSON[CreateUserRequest](c.Request().Body)
    if err != nil {
        return c.JSON(http.StatusBadRequest, err)
    }
    
    created := createUser(user)
    return c.JSON(http.StatusCreated, created)
})

With Rivaas App

import "rivaas.dev/app"

a := app.MustNew()

a.POST("/users", func(c *app.Context) error {
    var user CreateUserRequest
    if err := c.Bind(&user); err != nil {
        return err  // Automatically handled
    }
    
    created := createUser(user)
    return c.JSON(http.StatusCreated, created)
})

Next Steps

For complete API documentation, see API Reference.

10 - Examples

Real-world examples and integration patterns for common use cases

Complete, production-ready examples demonstrating common binding patterns and integrations.

Basic REST API

Complete CRUD handlers with proper error handling:

package main

import (
    "encoding/json"
    "net/http"
    "rivaas.dev/binding"
    "rivaas.dev/validation"
)

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
    Email    string `json:"email" validate:"required,email"`
    Age      int    `json:"age" validate:"required,min=18,max=120"`
}

type UpdateUserRequest struct {
    Username *string `json:"username,omitempty" validate:"omitempty,alphanum,min=3,max=32"`
    Email    *string `json:"email,omitempty" validate:"omitempty,email"`
    Age      *int    `json:"age,omitempty" validate:"omitempty,min=18,max=120"`
}

type ListUsersParams struct {
    Page     int      `query:"page" default:"1"`
    PageSize int      `query:"page_size" default:"20"`
    SortBy   string   `query:"sort_by" default:"created_at"`
    Search   string   `query:"search"`
    Tags     []string `query:"tags"`
}

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    // Bind JSON request
    req, err := binding.JSON[CreateUserRequest](r.Body)
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid request body", err)
        return
    }
    
    // Validate
    if err := validation.Validate(req); err != nil {
        respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
        return
    }
    
    // Create user
    user := &User{
        Username: req.Username,
        Email:    req.Email,
        Age:      req.Age,
    }
    if err := db.Create(user); err != nil {
        respondError(w, http.StatusInternalServerError, "Failed to create user", err)
        return
    }
    
    respondJSON(w, http.StatusCreated, user)
}

func UpdateUserHandler(w http.ResponseWriter, r *http.Request) {
    // Get user ID from path
    userID := chi.URLParam(r, "id")
    
    // Bind partial update
    req, err := binding.JSON[UpdateUserRequest](r.Body)
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid request body", err)
        return
    }
    
    // Validate
    if err := validation.Validate(req); err != nil {
        respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
        return
    }
    
    // Fetch existing user
    user, err := db.GetUser(userID)
    if err != nil {
        respondError(w, http.StatusNotFound, "User not found", err)
        return
    }
    
    // Apply updates (only non-nil fields)
    if req.Username != nil {
        user.Username = *req.Username
    }
    if req.Email != nil {
        user.Email = *req.Email
    }
    if req.Age != nil {
        user.Age = *req.Age
    }
    
    if err := db.Update(user); err != nil {
        respondError(w, http.StatusInternalServerError, "Failed to update user", err)
        return
    }
    
    respondJSON(w, http.StatusOK, user)
}

func ListUsersHandler(w http.ResponseWriter, r *http.Request) {
    // Bind query parameters
    params, err := binding.Query[ListUsersParams](r.URL.Query())
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid query parameters", err)
        return
    }
    
    // Fetch users with pagination
    users, total, err := db.ListUsers(params)
    if err != nil {
        respondError(w, http.StatusInternalServerError, "Failed to list users", err)
        return
    }
    
    // Response with pagination metadata
    response := map[string]interface{}{
        "data":       users,
        "total":      total,
        "page":       params.Page,
        "page_size":  params.PageSize,
        "total_pages": (total + params.PageSize - 1) / params.PageSize,
    }
    
    respondJSON(w, http.StatusOK, response)
}

// Helper functions
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func respondError(w http.ResponseWriter, status int, message string, err error) {
    response := map[string]interface{}{
        "error":   message,
        "details": err.Error(),
    }
    respondJSON(w, status, response)
}

Search API with Complex Filtering

Advanced search with multiple filter types:

type ProductSearchRequest struct {
    // Basic search
    Query string `query:"q"`
    
    // Pagination
    Page     int `query:"page" default:"1"`
    PageSize int `query:"page_size" default:"20"`
    
    // Sorting
    SortBy    string `query:"sort_by" default:"relevance"`
    SortOrder string `query:"sort_order" default:"desc"`
    
    // Filters (from JSON body for complex queries)
    Filters struct {
        Categories []string  `json:"categories"`
        Brands     []string  `json:"brands"`
        MinPrice   *float64  `json:"min_price"`
        MaxPrice   *float64  `json:"max_price"`
        InStock    *bool     `json:"in_stock"`
        MinRating  *int      `json:"min_rating"`
        Tags       []string  `json:"tags"`
        DateRange  *struct {
            From time.Time `json:"from"`
            To   time.Time `json:"to"`
        } `json:"date_range"`
    } `json:"filters"`
    
    // Metadata from headers
    Locale    string `header:"Accept-Language" default:"en-US"`
    Currency  string `header:"X-Currency" default:"USD"`
    UserAgent string `header:"User-Agent"`
}

func SearchProductsHandler(w http.ResponseWriter, r *http.Request) {
    // Multi-source binding: query + JSON + headers
    req, err := binding.Bind[ProductSearchRequest](
        binding.FromQuery(r.URL.Query()),
        binding.FromJSON(r.Body),
        binding.FromHeader(r.Header),
    )
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid request", err)
        return
    }
    
    // Build search query
    query := db.NewQuery().
        Search(req.Query).
        Page(req.Page, req.PageSize).
        Sort(req.SortBy, req.SortOrder)
    
    // Apply filters
    if len(req.Filters.Categories) > 0 {
        query = query.FilterCategories(req.Filters.Categories)
    }
    if len(req.Filters.Brands) > 0 {
        query = query.FilterBrands(req.Filters.Brands)
    }
    if req.Filters.MinPrice != nil {
        query = query.MinPrice(*req.Filters.MinPrice)
    }
    if req.Filters.MaxPrice != nil {
        query = query.MaxPrice(*req.Filters.MaxPrice)
    }
    if req.Filters.InStock != nil && *req.Filters.InStock {
        query = query.InStockOnly()
    }
    if req.Filters.MinRating != nil {
        query = query.MinRating(*req.Filters.MinRating)
    }
    if req.Filters.DateRange != nil {
        query = query.DateRange(req.Filters.DateRange.From, req.Filters.DateRange.To)
    }
    
    // Execute search
    results, total, err := query.Execute(r.Context())
    if err != nil {
        respondError(w, http.StatusInternalServerError, "Search failed", err)
        return
    }
    
    // Apply currency conversion if needed
    if req.Currency != "USD" {
        results = convertCurrency(results, req.Currency)
    }
    
    response := map[string]interface{}{
        "results":     results,
        "total":       total,
        "page":        req.Page,
        "page_size":   req.PageSize,
        "total_pages": (total + req.PageSize - 1) / req.PageSize,
    }
    
    respondJSON(w, http.StatusOK, response)
}

Multi-Tenant API

Handle tenant context from headers:

type TenantRequest struct {
    TenantID string `header:"X-Tenant-ID" validate:"required,uuid"`
    APIKey   string `header:"X-API-Key" validate:"required"`
}

type CreateResourceRequest struct {
    TenantRequest
    Name        string `json:"name" validate:"required"`
    Description string `json:"description"`
    Type        string `json:"type" validate:"required,oneof=typeA typeB typeC"`
}

func CreateResourceHandler(w http.ResponseWriter, r *http.Request) {
    // Bind headers + JSON
    req, err := binding.Bind[CreateResourceRequest](
        binding.FromHeader(r.Header),
        binding.FromJSON(r.Body),
    )
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid request", err)
        return
    }
    
    // Validate
    if err := validation.Validate(req); err != nil {
        respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
        return
    }
    
    // Verify tenant and API key
    tenant, err := auth.VerifyTenant(req.TenantID, req.APIKey)
    if err != nil {
        respondError(w, http.StatusUnauthorized, "Invalid tenant credentials", err)
        return
    }
    
    // Create resource in tenant context
    resource := &Resource{
        TenantID:    tenant.ID,
        Name:        req.Name,
        Description: req.Description,
        Type:        req.Type,
    }
    
    if err := db.Create(resource); err != nil {
        respondError(w, http.StatusInternalServerError, "Failed to create resource", err)
        return
    }
    
    respondJSON(w, http.StatusCreated, resource)
}

File Upload with Metadata

Handle multipart form data:

type FileUploadRequest struct {
    Title       string   `form:"title" validate:"required"`
    Description string   `form:"description"`
    Tags        []string `form:"tags"`
    Public      bool     `form:"public"`
}

func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
    // Parse multipart form (32MB max)
    if err := r.ParseMultipartForm(32 << 20); err != nil {
        respondError(w, http.StatusBadRequest, "Failed to parse form", err)
        return
    }
    
    // Bind form fields
    req, err := binding.Form[FileUploadRequest](r.Form)
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid form data", err)
        return
    }
    
    // Validate
    if err := validation.Validate(req); err != nil {
        respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
        return
    }
    
    // Get uploaded file
    file, header, err := r.FormFile("file")
    if err != nil {
        respondError(w, http.StatusBadRequest, "Missing or invalid file", err)
        return
    }
    defer file.Close()
    
    // Validate file type
    if !isAllowedFileType(header.Header.Get("Content-Type")) {
        respondError(w, http.StatusBadRequest, "Invalid file type", nil)
        return
    }
    
    // Save file
    savedFile, err := storage.SaveFile(file, header.Filename)
    if err != nil {
        respondError(w, http.StatusInternalServerError, "Failed to save file", err)
        return
    }
    
    // Create database record
    record := &FileRecord{
        Title:       req.Title,
        Description: req.Description,
        Tags:        req.Tags,
        Public:      req.Public,
        Filename:    savedFile.Name,
        Size:        savedFile.Size,
        ContentType: header.Header.Get("Content-Type"),
    }
    
    if err := db.Create(record); err != nil {
        respondError(w, http.StatusInternalServerError, "Failed to create record", err)
        return
    }
    
    respondJSON(w, http.StatusCreated, record)
}

Webhook Handler with Signature Verification

Process webhooks with headers:

type WebhookRequest struct {
    Signature string    `header:"X-Webhook-Signature" validate:"required"`
    Timestamp time.Time `header:"X-Webhook-Timestamp" validate:"required"`
    Event     string    `header:"X-Webhook-Event" validate:"required"`
    
    Payload json.RawMessage `json:"-"`
}

func WebhookHandler(w http.ResponseWriter, r *http.Request) {
    // Read body for signature verification
    body, err := io.ReadAll(r.Body)
    if err != nil {
        respondError(w, http.StatusBadRequest, "Failed to read body", err)
        return
    }
    r.Body = io.NopCloser(bytes.NewReader(body))
    
    // Bind headers
    req, err := binding.Header[WebhookRequest](r.Header)
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid headers", err)
        return
    }
    
    // Validate
    if err := validation.Validate(req); err != nil {
        respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
        return
    }
    
    // Verify signature
    if !verifyWebhookSignature(body, req.Signature, webhookSecret) {
        respondError(w, http.StatusUnauthorized, "Invalid signature", nil)
        return
    }
    
    // Check timestamp (prevent replay attacks)
    if time.Since(req.Timestamp) > 5*time.Minute {
        respondError(w, http.StatusBadRequest, "Request too old", nil)
        return
    }
    
    // Store raw payload
    req.Payload = body
    
    // Process event
    switch req.Event {
    case "payment.success":
        var payment PaymentEvent
        if err := json.Unmarshal(body, &payment); err != nil {
            respondError(w, http.StatusBadRequest, "Invalid payment payload", err)
            return
        }
        handlePaymentSuccess(payment)
        
    case "payment.failed":
        var payment PaymentEvent
        if err := json.Unmarshal(body, &payment); err != nil {
            respondError(w, http.StatusBadRequest, "Invalid payment payload", err)
            return
        }
        handlePaymentFailed(payment)
        
    default:
        respondError(w, http.StatusBadRequest, "Unknown event type", nil)
        return
    }
    
    w.WriteHeader(http.StatusNoContent)
}

GraphQL-style Nested Queries

Handle complex nested structures:

type GraphQLRequest struct {
    Query     string                 `json:"query" validate:"required"`
    Variables map[string]interface{} `json:"variables"`
    OperationName string              `json:"operationName"`
}

type GraphQLResponse struct {
    Data   interface{}            `json:"data,omitempty"`
    Errors []GraphQLError         `json:"errors,omitempty"`
}

type GraphQLError struct {
    Message string `json:"message"`
    Path    []string `json:"path,omitempty"`
}

func GraphQLHandler(w http.ResponseWriter, r *http.Request) {
    req, err := binding.JSON[GraphQLRequest](r.Body)
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid GraphQL request", err)
        return
    }
    
    // Validate
    if err := validation.Validate(req); err != nil {
        respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
        return
    }
    
    // Execute GraphQL query
    result := executeGraphQL(r.Context(), req.Query, req.Variables, req.OperationName)
    
    respondJSON(w, http.StatusOK, result)
}

Batch Operations

Process multiple items in one request:

type BatchCreateRequest []CreateUserRequest

type BatchResponse struct {
    Success []User       `json:"success"`
    Failed  []BatchError `json:"failed"`
}

type BatchError struct {
    Index int    `json:"index"`
    Item  interface{} `json:"item"`
    Error string `json:"error"`
}

func BatchCreateUsersHandler(w http.ResponseWriter, r *http.Request) {
    // Bind array of requests
    batch, err := binding.JSON[BatchCreateRequest](r.Body)
    if err != nil {
        respondError(w, http.StatusBadRequest, "Invalid batch request", err)
        return
    }
    
    // Validate batch size
    if len(batch) == 0 {
        respondError(w, http.StatusBadRequest, "Empty batch", nil)
        return
    }
    if len(batch) > 100 {
        respondError(w, http.StatusBadRequest, "Batch too large (max 100)", nil)
        return
    }
    
    response := BatchResponse{
        Success: make([]User, 0),
        Failed:  make([]BatchError, 0),
    }
    
    // Process each item
    for i, req := range batch {
        // Validate item
        if err := validation.Validate(req); err != nil {
            response.Failed = append(response.Failed, BatchError{
                Index: i,
                Item:  req,
                Error: err.Error(),
            })
            continue
        }
        
        // Create user
        user := &User{
            Username: req.Username,
            Email:    req.Email,
            Age:      req.Age,
        }
        
        if err := db.Create(user); err != nil {
            response.Failed = append(response.Failed, BatchError{
                Index: i,
                Item:  req,
                Error: err.Error(),
            })
            continue
        }
        
        response.Success = append(response.Success, *user)
    }
    
    // Return 207 Multi-Status if there were any failures
    status := http.StatusCreated
    if len(response.Failed) > 0 {
        status = http.StatusMultiStatus
    }
    
    respondJSON(w, status, response)
}

Integration with Rivaas App

Complete application setup:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "rivaas.dev/app"
    "rivaas.dev/binding"
    "rivaas.dev/router"
)

func main() {
    // Create app
    a := app.MustNew(
        app.WithServiceName("api-server"),
        app.WithServiceVersion("1.0.0"),
    )
    
    // Setup routes
    setupRoutes(a)
    
    // Graceful shutdown
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()
    
    // Start server
    addr := ":8080"
    log.Printf("Server starting on %s", addr)
    
    if err := a.Start(ctx, addr); err != nil {
        log.Fatal(err)
    }
}

func setupRoutes(a *app.App) {
    // Users
    a.POST("/users", CreateUserHandler)
    a.GET("/users", ListUsersHandler)
    a.GET("/users/:id", GetUserHandler)
    a.PATCH("/users/:id", UpdateUserHandler)
    a.DELETE("/users/:id", DeleteUserHandler)
    
    // Search
    a.POST("/search", SearchProductsHandler)
}

func CreateUserHandler(c *router.Context) error {
    req, err := binding.JSON[CreateUserRequest](c.Request().Body)
    if err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": err.Error(),
        })
    }
    
    user := createUser(req)
    return c.JSON(http.StatusCreated, user)
}

Next Steps

For complete API documentation, see API Reference.