Skip to content

Repository files navigation

Go-Reqbind

Go Reference License

Go-Reqbind is a Go library that automatically unmarshals HTTP requests into structured Go types and validates them using go-playground/validator. It supports extracting data from headers, query parameters, path parameters, and request bodies -- all driven by struct tags.

Table of Contents

Features

  • 🔄 Automatic Unmarshaling: Extract data from headers, query parameters, path parameters, and request bodies into Go structs
  • Built-in Validation: Integrated with go-playground/validator for robust validation
  • 🎯 Custom Validations: Support for custom validation functions
  • 📝 Nested Structures: Handle complex nested query parameters and JSON bodies
  • 🔍 Detailed Error Messages: Get clear, structured validation error messages
  • 🏷️ Flexible Tagging: Use struct tags to map request data to struct fields

Installation

go get go.companyinfo.dev/go-reqbind

Quick Start

package main

import (
    "net/http"
    "go.companyinfo.dev/go-reqbind"
)

type CreateUserRequest struct {
    Body struct {
        Name  string `json:"name" validate:"required,min=3"`
        Email string `json:"email" validate:"required,email"`
    } `goReq:"body"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    req := &CreateUserRequest{}
    
    validationErrors, err := reqbind.UnmarshalAndValidate(*r, req, nil)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    if validationErrors != nil {
        // Handle validation errors
        return
    }
    
    // Use req.Body.Name and req.Body.Email
}

Usage

Basic Concepts

Go-Reqbind uses struct tags to identify which parts of the HTTP request should be unmarshaled:

  • goReq:"header" - Extract from HTTP headers
  • goReq:"query" - Extract from query parameters
  • goReq:"path" - Extract from path parameters (requires URL template)
  • goReq:"body" - Extract from request body (JSON)

1. Query Parameters

Go-Reqbind supports nested query parameters using bracket notation (e.g., filter[user][name]=value).

Example 1: Simple Query Parameters

type SearchRequest struct {
    QueryParams struct {
        Name   string
        Page   int
        Limit  int
    } `goReq:"query"`
}

// URL: /search?name=john&page=1&limit=10

Example 2: Nested Query Parameters

type FilterRequest struct {
    QueryParams struct {
        Filter struct {
            User struct {
                Name  string
                Email string
            }
        }
        Page struct {
            Size   int
            Number int
        }
    } `goReq:"query"`
}

// URL: /users?filter[user][name]=John&filter[user][email]=john@example.com&page[size]=20&page[number]=1

Example 3: Using Custom Field Names with Tags

You can use the goReq tag to map query parameters to different struct field names:

type MyRequest struct {
    QueryParams struct {
        Name   string `goReq:"name"`
        Filter struct {
            User struct {
                Id        string `goReq:"userID"`
                Profile struct {
                    Age struct {
                        Min uint8 `goReq:"min"`
                        Max uint8 `goReq:"max"`
                    } `goReq:"ageRange"`
                } `goReq:"profile"`
            }
        }
    } `goReq:"query"`
}

// URL: /users?name=john&filter[user][userID]=123&filter[user][profile][age_range][min]=18&filter[user][profile][age_range][max]=65

Example 4: Arrays in Query Parameters

type SearchRequest struct {
    QueryParams struct {
        Filter struct {
            User struct {
                Interests []string
                Tags      []string
            }
        }
    } `goReq:"query"`
}

// URL: /search?filter[user][interests]=coding,reading,traveling&filter[user][tags]=developer,go

Example 5: Pointers for Optional Values

type FilterRequest struct {
    QueryParams struct {
        Filter struct {
            Year struct {
                Min *int
                Max *int
            }
        }
    } `goReq:"query"`
}

// URL: /search?filter[year][min]=2021
// Min will be set to 2021, Max will be nil

Example 6: Maps in Query Parameters

type SearchRequest struct {
    QueryParams struct {
        Filter struct {
            Metadata map[string]any
        }
    } `goReq:"query"`
}

// URL: /search?filter[metadata][reportType]=general-report&filter[metadata][domain]=nl

2. Headers

Extract HTTP headers into struct fields. Header matching is case-insensitive.

type MyRequest struct {
    Header struct {
        UserID         string   `goReq:"X-User-Id" validate:"required"`
        ContentType    string   `goReq:"Content-Type"`
        AcceptEncoding []string `goReq:"Accept-Encoding"`
    } `goReq:"header"`
}

// Headers:
// X-User-Id: user-123
// Content-Type: application/json
// Accept-Encoding: gzip, deflate, br

Note:

  • Header names are matched case-insensitively
  • Multiple values for the same header are joined with commas for strings, or stored as a slice for slice fields
  • Use goReq:"-" to skip a field during unmarshaling but still validate it

3. Path Parameters

Path parameters require a URL template to extract values from the URL path.

type GetUserProfileRequest struct {
    PathParams struct {
        UserID string `goReq:"userID" validate:"uuid"`
        Year   string `goReq:"year"`
    } `goReq:"path"`
}

// URL: /api/users/6f3c1b3e-9d42-4f7a-8b8a-3b8d2e2e7c91/profile/2024
// Template: /api/users/:userID/profile/:year

options := &reqbind.Options{
    URLTemplate: "/api/users/:userID/profile/:year",
}
validationErrors, err := reqbind.UnmarshalAndValidate(*r, req, options)

4. Request Body

Extract JSON request body into struct fields. Uses standard json tags.

type CreateUserProfileRequest struct {
    Body struct {
        FirstName string `json:"firstName" validate:"required,min=2"`
        LastName  string `json:"lastName" validate:"required,min=2"`
        Email     string `json:"email" validate:"required,email"`
        Phone     string `json:"phone,omitempty"`
        Address   struct {
            Street   string `json:"street,omitempty"`
            City     string `json:"city,omitempty"`
            Postcode string `json:"postcode,omitempty"`
            Country  string `json:"country,omitempty"`
        } `json:"address,omitempty"`
    } `goReq:"body"`
}

// Request Body:
// {
//   "firstName": "John",
//   "lastName": "Doe",
//   "email": "john@example.com",
//   "phone": "+1234567890",
//   "address": {
//     "street": "123 Main St",
//     "city": "New York",
//     "postcode": "10001",
//     "country": "USA"
//   }
// }

5. Complete Example: Combining All Request Parts

type GetUserRequest struct {
    Header struct {
        UserID         string   `goReq:"X-User-Id" validate:"required"`
        ContentType    string   `goReq:"Content-Type"`
        AcceptEncoding []string `goReq:"Accept-Encoding"`
    } `goReq:"header"`
    
    PathParams struct {
        UserID string `goReq:"userID" validate:"uuid"`
    } `goReq:"path"`
    
    QueryParams struct {
        Filter struct {
            User struct {
                Name    string `validate:"required"`
                Profile struct {
                    FirstName string `validate:"required,min=2,max=50"`
                    LastName  string `validate:"required,min=2,max=50"`
                    Email     string `validate:"required,email"`
                    Age       uint8  `validate:"required,numeric,gte=18,lte=120"`
                    Gender    string `validate:"omitempty,oneof=male female prefer_not_to"`
                }
                Address struct {
                    Postcode string `validate:"omitempty,len=5"`
                }
            }
        }
        Page struct {
            Size   int
            Number int
        }
    } `goReq:"query"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    req := &GetUserRequest{}
    
    options := &reqbind.Options{
        URLTemplate: "/api/users/:userID",
    }
    
    validationErrors, err := reqbind.UnmarshalAndValidate(*r, req, options)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    if validationErrors != nil {
        // Return validation errors
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(validationErrors)
        return
    }
    
    // Process the request
    // Access: req.Header.UserID, req.PathParams.UserID, req.QueryParams.Filter.User.Name, etc.
}

Validation

Go-Reqbind integrates with go-playground/validator for validation. Use the validate tag on struct fields.

Common Validation Tags

  • required - Field must be present and not empty
  • email - Must be a valid email address
  • min=3 - Minimum length/value
  • max=100 - Maximum length/value
  • numeric - Must be numeric
  • uuid - Must be a valid UUID
  • oneof=value1 value2 - Must be one of the specified values
  • omitempty - Validate only if field is not empty
  • dive - Validate nested structs/slices

Validation Error Response

Validation errors are returned as a slice of ValidationError structs:

type ValidationError struct {
    Detail string `json:"detail"`
    Source struct {
        Pointer   string `json:"pointer,omitempty"`   // For body errors (JSON pointer)
        Parameter string `json:"parameter,omitempty"` // For query/path errors
        Header    string `json:"header,omitempty"`    // For header errors
    } `json:"source,omitempty"`
}

Example: Handling Validation Errors

validationErrors, err := reqbind.UnmarshalAndValidate(*r, req, options)
if err != nil {
    // Handle unmarshaling error
    return
}

if validationErrors != nil {
    for _, ve := range validationErrors {
        fmt.Printf("Error in %s: %s\n", ve.Source.Parameter, ve.Detail)
    }
    return
}

Custom Validations

You can register custom validation functions:

// Define custom validation function
func isEven(fl validator.FieldLevel) bool {
    if num, ok := fl.Field().Interface().(int); ok {
        return num%2 == 0
    }
    return false
}

// Use in request struct
type MyRequest struct {
    Body struct {
        Age int `json:"age" validate:"is-even"`
    } `goReq:"body"`
}

// Register custom validation
options := &reqbind.Options{
    CustomValidations: map[string]validator.Func{
        "is-even": isEven,
    },
}

validationErrors, err := reqbind.UnmarshalAndValidate(*r, req, options)

Example: Validation with Nested Slices

type MyRequest struct {
    Body struct {
        Users []struct {
            Profiles []struct {
                Name string `json:"name" validate:"required,min=4"`
            } `json:"profiles" validate:"required,dive"`
        } `json:"users" validate:"required,dive"`
    } `goReq:"body"`
}

// Request Body:
// {
//   "users": [
//     {
//       "profiles": [
//         {"name": "John Doe"},
//         {"name": "Bob"}  // This will fail validation (min=4)
//       ]
//     }
//   ]
// }

API Reference

Functions

Unmarshal(request http.Request, target any, urlTemplate string) error

Unmarshals an HTTP request into the target struct without validation.

Parameters:

  • request: The HTTP request to unmarshal
  • target: Pointer to a struct that will be populated
  • urlTemplate: URL template for path parameter extraction (e.g., /users/:id)

Returns:

  • error: Error if unmarshaling fails

UnmarshalAndValidate(request http.Request, target any, options *Options) ([]ValidationError, error)

Unmarshals an HTTP request into the target struct and validates it.

Parameters:

  • request: The HTTP request to unmarshal
  • target: Pointer to a struct that will be populated
  • options: Optional configuration (can be nil)

Returns:

  • []ValidationError: Slice of validation errors (nil if validation passes)
  • error: Error if unmarshaling fails

Options Struct

type Options struct {
    URLTemplate       string                    // URL template for path parameters
    CustomValidations map[string]validator.Func // Custom validation functions
}

Field Name Resolution

Go-Reqbind uses the following priority to resolve field names:

  1. Struct tag (goReq tag) - If present, uses the tag value
  2. Field name - Converts the query parameter name to TitleCase and matches against struct field names

For example:

  • Query param name matches field Name or field with tag goReq:"name"
  • Query param filter[user][email] matches nested struct Filter.User.Email or field with tag goReq:"email"

Supported Types

Query Parameters

  • string
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • float32, float64
  • []string (comma-separated values)
  • *int (pointers for optional values)
  • map[string]any

Headers

  • string
  • int
  • []string

Path Parameters

  • string
  • int

Request Body

  • Any type supported by encoding/json

Error Handling

Unmarshaling Errors

If unmarshaling fails (e.g., invalid JSON, malformed query string), Unmarshal and UnmarshalAndValidate return an error.

Validation Errors

If validation fails, UnmarshalAndValidate returns a slice of ValidationError structs with detailed information about each validation failure.

Examples

For complete, runnable examples covering all features, see the examples/ directory.

Contributing

Contributions are welcome! Here's how you can contribute:

  1. Fork the repository
  2. Create a new branch (git checkout -b feature/improvement)
  3. Make your changes
  4. Commit your changes (git commit -am 'Add new feature')
  5. Push to the branch (git push origin feature/improvement)
  6. Create a Pull Request

Please make sure to:

  • Follow the existing code style
  • Add tests if applicable
  • Update documentation as needed
  • Include a clear description of your changes in the PR

License

This project is licensed under the Apache License 2.0.

Acknowledgments

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages