A lightweight Go library for decoding application/x-www-form-urlencoded HTTP request bodies into Go structs or maps. Uses struct tags for field mapping and supports custom type unmarshalling.
- Decode form data directly from
http.Requestorurl.Values - Support for all Go primitive types (strings, integers, floats, booleans, complex numbers)
- Struct tag-based field mapping with flexible options
- Custom type unmarshalling via the
Unmarshallerinterface - Map decoding support
- Detailed error types for parse failures and missing required fields
- Zero external dependencies (except for testing)
go get github.com/ElOrlis/pformpackage main
import (
"fmt"
"net/http"
"github.com/ElOrlis/pform"
)
type LoginForm struct {
Username string `form:"username,required"`
Password string `form:"password,required"`
Remember bool `form:"remember,omitempty"`
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
decoder, err := pform.NewFormUrlDecoder(r)
if err != nil {
http.Error(w, "Invalid content type", http.StatusBadRequest)
return
}
var form LoginForm
if err := decoder.Decode(&form); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Printf("Login attempt: %s\n", form.Username)
}package main
import (
"fmt"
"net/url"
"github.com/ElOrlis/pform"
)
type UserProfile struct {
Name string `form:"name"`
Age int `form:"age"`
Email string `form:"email,omitempty"`
Balance float64 `form:"balance"`
}
func main() {
values := url.Values{
"name": {"John Doe"},
"age": {"30"},
"balance": {"1234.56"},
}
var profile UserProfile
decoder := pform.NewDecoder(values)
if err := decoder.Decode(&profile); err != nil {
panic(err)
}
fmt.Printf("Name: %s, Age: %d, Balance: %.2f\n",
profile.Name, profile.Age, profile.Balance)
}The form struct tag controls how fields are mapped and validated:
type Example struct {
// Maps to form field "field_name"
Field1 string `form:"field_name"`
// Skip empty values (no error if missing)
Field2 string `form:"field_name,omitempty"`
// Return error if field is missing
Field3 string `form:"field_name,required"`
// Ignore this field completely
Field4 string `form:"-"`
// Fields without tags are ignored
Field5 string
}| Option | Description |
|---|---|
form:"name" |
Maps the struct field to form field "name" |
form:"name,omitempty" |
Skip if the form field is empty or missing |
form:"name,required" |
Return RequiredFieldError if the field is missing |
form:"-" |
Ignore this field during decoding |
| Type | Example Values |
|---|---|
string |
"hello", "John Doe" |
int, int8, int16, int32, int64 |
"42", "-100" |
uint, uint8, uint16, uint32, uint64 |
"42", "255" |
float32, float64 |
"3.14", "1.23e-4" |
complex64, complex128 |
"(1+2i)", "3.14", "5i" |
bool |
"true", "false", "1", "0", "t", "f" |
Decode form data directly into a map:
values := url.Values{
"key1": {"value1"},
"key2": {"value2"},
}
var data map[string]string
decoder := pform.NewDecoder(values)
err := decoder.Decode(&data)
// data = map[string]string{"key1": "value1", "key2": "value2"}Maps support any value type that can be parsed:
var counts map[string]int
var scores map[string]float64Note: Map keys must be of type string.
Implement the Unmarshaller interface to handle custom type parsing:
type Unmarshaller interface {
UnmarshalValue(string) error
}type Date struct {
Year int
Month int
Day int
}
func (d *Date) UnmarshalValue(v string) error {
t, err := time.Parse("2006-01-02", v)
if err != nil {
return err
}
d.Year = t.Year()
d.Month = int(t.Month())
d.Day = t.Day()
return nil
}
type Event struct {
Name string `form:"name"`
Date Date `form:"date"`
}type Status int
const (
StatusPending Status = iota
StatusActive
StatusInactive
)
func (s *Status) UnmarshalValue(v string) error {
switch strings.ToLower(v) {
case "pending":
*s = StatusPending
case "active":
*s = StatusActive
case "inactive":
*s = StatusInactive
default:
return fmt.Errorf("invalid status: %s", v)
}
return nil
}The library provides two custom error types for detailed error information:
Returned when a value cannot be converted to the target type:
var parseErr pform.ParseError
if errors.As(err, &parseErr) {
fmt.Printf("Field: %s\n", parseErr.Field) // Field name
fmt.Printf("Type: %s\n", parseErr.Type) // Expected type
fmt.Printf("Value: %s\n", parseErr.Value) // Provided value
fmt.Printf("Cause: %v\n", parseErr.Err) // Underlying error
}Returned when a required field is missing:
var reqErr pform.RequiredFieldError
if errors.As(err, &reqErr) {
fmt.Printf("Missing required field: %s\n", reqErr.Error())
}func handleForm(r *http.Request) error {
decoder, err := pform.NewFormUrlDecoder(r)
if err != nil {
return fmt.Errorf("invalid request: %w", err)
}
var form MyForm
if err := decoder.Decode(&form); err != nil {
var parseErr pform.ParseError
var reqErr pform.RequiredFieldError
switch {
case errors.As(err, &reqErr):
return fmt.Errorf("missing required field: %w", err)
case errors.As(err, &parseErr):
return fmt.Errorf("invalid value for %s: %w", parseErr.Field, err)
default:
return fmt.Errorf("decode error: %w", err)
}
}
return nil
}func NewFormUrlDecoder(r *http.Request) (XFormUrlEncoded, error)Creates a decoder from an HTTP request. Validates that the Content-Type is application/x-www-form-urlencoded.
func NewDecoder(form url.Values) DecoderCreates a decoder from url.Values directly.
type Decoder struct {
// contains filtered or unexported fields
}
func (d Decoder) Decode(dest any) errorDecodes form values into the destination struct or map. The destination must be a pointer to a struct or map.
type XFormUrlEncoded interface {
Decode(dest any) error
}Interface implemented by the decoder.
type Unmarshaller interface {
UnmarshalValue(string) error
}Implement this interface to provide custom parsing logic for your types.
The NewFormUrlDecoder function validates the HTTP request's Content-Type header:
- Must be
application/x-www-form-urlencoded - Accepts charset parameters (e.g.,
application/x-www-form-urlencoded; charset=utf-8) - Case-insensitive matching
- Trims whitespace
Run the test suite:
# Run all tests
go test ./...
# Run with verbose output
go test -v ./...
# Run with race detection
go test -race ./...
# Run with coverage
go test -cover ./...
# Run benchmarks
go test -bench=. ./...Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Write tests for new functionality
- Ensure all tests pass with
go test ./... - Run
go vet ./...andstaticcheck ./...before submitting - Format code with
gofmt
This project uses LLM assistance exclusively for:
- Testing - Generating test cases and test coverage
- CI/CD Workflows - Creating and maintaining GitHub Actions workflows
- Documentation - Writing and improving documentation
The core library code is human-written without LLM assistance.
This project is licensed under the MIT License - see the LICENSE file for details.
- gorilla/schema - Package for converting structs to and from form values
- go-playground/form - Decodes url.Values into Go value(s) and Encodes Go value(s) into url.Values