form

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 13, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package form binds and validates HTML form submissions into Go structs.

Bind is the headline API: it parses an *http.Request body, decodes the form values into a destination struct (using "form" struct tags via gorilla/schema), runs declarative validation (using "validate" struct tags via go-playground/validator), and optionally runs bespoke cross-field rules via the Validator interface. User-facing problems (a non-numeric value in a numeric field, a missing required field, a malformed email, and so on) are reported as an Errors value keyed by form field name. Only genuinely unprocessable requests (a body that exceeds the size limit, an unparseable content type) are reported as a returned error, which a handler should translate into an HTTP 400 response.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Errors

type Errors map[string][]string

Errors maps an HTML form field name to its human-readable validation messages. The keys are the form field names (the value of the struct's "form" tag), which makes Errors convenient to consume directly from HTML templates when re-rendering a form with inline error messages.

The zero value of Errors is not usable for writing; construct one with make(Errors) or rely on Bind to allocate it. Read methods (Get, Has, Any) are safe to call on a nil Errors.

func Bind

func Bind(r *http.Request, dst any, opts ...Option) (Errors, error)

Bind parses, decodes, and validates the form submission in r into dst.

dst must be a non-nil pointer to a struct. Its fields are populated from the request's POST form values using "form" struct tags, then validated using "validate" struct tags. If dst (or the struct it points to) implements Validator, its Validate method is invoked and the resulting errors are merged in.

Bind distinguishes two failure modes:

  • User errors (a wrong type for a field, a failed validation rule) are returned in the Errors result keyed by form field name. In this case the returned error is nil. Callers test Errors.Any to decide whether to re-render the form.
  • Unprocessable requests (a body that exceeds the configured size limit, a malformed or unparseable body) are reported via the returned error. In this case the Errors result is nil. Handlers should respond with HTTP 400 (Bad Request).

Callers should check the returned error first and respond 400 if it is non-nil; only then consult Errors.Any to decide whether to re-render. The Errors result is safe to use even when nil or empty (its read methods are nil-safe), so the two checks must not be collapsed: a non-nil error with a nil Errors would make a bare Errors.Any check silently skip the 400.

Only "form"-tagged fields are populated, but every such field is settable by the client. Bind to a struct that contains only user-supplied inputs; never bind directly to a domain or persistence model, or a client could set fields (an ID, an ownership or role flag) by posting extra form keys.

Example

ExampleBind demonstrates binding a form submission, then printing the per-field validation errors in a deterministic order.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"sort"
	"strings"

	"github.com/mikehelmick/go-bananas/form"
)

// Signup is a destination struct with form and validate tags.
type Signup struct {
	Name  string `form:"name" validate:"required"`
	Email string `form:"email" validate:"required,email"`
	Age   int    `form:"age" validate:"gte=18"`
}

// ExampleBind demonstrates binding a form submission, then printing the
// per-field validation errors in a deterministic order.
func main() {
	// Build a request with a missing name, a malformed email, and an
	// under-age value.
	values := url.Values{}
	values.Set("email", "nope")
	values.Set("age", "12")
	r := httptest.NewRequest(http.MethodPost, "/signup", strings.NewReader(values.Encode()))
	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	var dst Signup
	errs, err := form.Bind(r, &dst)
	if err != nil {
		// An unprocessable request (oversized body, malformed content type).
		fmt.Println("bad request:", err)
		return
	}

	if !errs.Any() {
		fmt.Println("ok")
		return
	}

	// Sort field names for stable output.
	fields := make([]string, 0, len(errs))
	for field := range errs {
		fields = append(fields, field)
	}
	sort.Strings(fields)
	for _, field := range fields {
		for _, msg := range errs.Get(field) {
			fmt.Printf("%s: %s\n", field, msg)
		}
	}

}
Output:
age: is too small
email: must be a valid email address
name: is required

func (Errors) Add

func (e Errors) Add(field, msg string)

Add appends msg to the list of messages associated with field. It must be called on an initialized (non-nil) Errors; Bind always passes an initialized map, and a Validator implementation should construct one with make(Errors).

func (Errors) Any

func (e Errors) Any() bool

Any reports whether any field has at least one message recorded. It is the canonical way to ask "did validation fail?" after a call to Bind.

func (Errors) Get

func (e Errors) Get(field string) []string

Get returns the messages recorded for field, or nil if there are none. The nil return is intentional: ranging over a nil slice in a template (or in Go) is a no-op, so callers can write {{range .Errors.Get "email"}} without a guard.

func (Errors) Has

func (e Errors) Has(field string) bool

Has reports whether field has at least one message recorded.

func (Errors) Merge

func (e Errors) Merge(o Errors)

Merge copies all of the messages in o into e, appending to any existing messages for a given field. It is used to fold bespoke Validator results over tag-based results. Merging a nil or empty o is a no-op.

type Option

type Option func(*options)

Option configures the behavior of Bind.

func WithMaxBodyBytes

func WithMaxBodyBytes(n int64) Option

WithMaxBodyBytes sets the maximum number of bytes Bind will read from the request body. A request whose body exceeds this limit is treated as unprocessable and causes Bind to return a non-nil error. The default is 10 MiB. A non-positive value is ignored and the default is retained.

The limit is only enforced if nothing has already read the body: net/http's ParseForm is idempotent, so if an earlier middleware parsed the form (for example a CSRF middleware that reads a posted token), that read used the standard library's own limit and Bind's cap no longer applies. To bound body size reliably regardless of middleware ordering, wrap the request earlier in the chain with http.MaxBytesHandler.

func WithMessages

func WithMessages(m map[string]string) Option

WithMessages overrides the human-readable messages used for one or more validation tags. The keys are validator tag names (for example "required" or "email"); the special key "default" overrides the fallback message. Tags not present in m fall back to the built-in defaults.

type Validator

type Validator interface {
	Validate() Errors
}

Validator is implemented by destination structs that need cross-field or otherwise bespoke validation rules that the "validate" struct tags cannot express cleanly. It is optional: Bind only invokes Validate when the destination (or the value it points to) implements the interface.

Validate returns an Errors value keyed by form field name. The returned errors are merged over (appended to) any errors already produced by tag validation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL