binder / Docs / Binding sources

Binding sources

A field's tag says where its value comes from. There are seven tags, and a field may carry more than one — in which case the first in the order below wins.

TagReadsNotes
path:"id"Path parametersVia Request.PathValue, so the router must have declared {id}.
query:"page"URL query stringRead from the raw query string for the keys fields ask for. A query of more than 64 parameters is parsed into a map once instead; one beyond net/url's limit of 10,000 parameters binds nothing from the query.
body:"email"Request bodyJSON, form-encoded or multipart, chosen by Content-Type.
json:"email"Request bodyFor structs whose tags are shared with serialisation.
cookie:"session"Request cookiesName matched exactly, as cookies are case-sensitive.
header:"X-Request-ID"Request headersMatched case-insensitively.
form:"page"Form body, then query stringAs r.FormValue reads: the body's value when it has the key, otherwise the query's.

Precedence runs path, query, body, json, cookie, header, form. A field tagged both query and header is read from the query string; the header is not consulted, even when the query has no such key.

Fields with no binding tag are left alone, and so are unexported fields — reflection cannot set them, so a tag on one is silently ignored rather than an error.

Other frameworks' tags are left alone too, with three exceptions: Gin's binding and uri and Echo's param. A field carrying one was meant to bind, and would otherwise be left unset without a word, so binding refuses the type with ErrInvalidTarget, naming the field and what to use instead: path: for uri: and param:, and the required option and a Validate method for binding:. A validate: tag is not refused, since a Validate method may hand it to a validation library.

Path parameters

binder does not route. It reads what net/http already resolved, which means the pattern registered on the mux and the path: tag have to agree on the name:

mux.HandleFunc("GET /orgs/{org}/repos/{repo}", show)

type Show struct {
	Org  string `path:"org"`
	Repo string `path:"repo"`
}

A path parameter the pattern never declared reads as empty. Combined with required that surfaces as a missing value rather than a mysterious zero, which is usually how a typo in either name gets found.

Query parameters

// GET /search?q=binder&page=2&tag=go&tag=http
type Search struct {
	Q    string   `query:"q"`
	Page int      `query:"page"`
	Tags []string `query:"tag"`
}

For path, query and header an empty value counts as missing: binder treats ?q= exactly as no q at all. For a slice that holds when every value given is empty; an empty value among others is an element, and an error for a non-string element type. An empty value leaves the field untouched, so a pointer stays nil, a default survives, and ?page= on an int field is not a parse error; with required it is reported as missing.

Body: body and json

body: is the tag to reach for. It is content-type aware, so the same struct binds a JSON API call and an HTML form post:

type Signup struct {
	Email    string `body:"email,required"`
	Password string `body:"password,required"`
	Referrer string `body:"ref,omitempty"`
}

JSON is recognised by media type, including the RFC 6839 suffix form — application/json, text/json, application/vnd.api+json, application/hal+json and application/problem+json all parse as JSON. Form bodies need application/x-www-form-urlencoded, and multipart bodies multipart/form-data. A body of any other type is not parsed, and the request binds from its other sources; it is still read, within the size limit, when a field binds from the body.

A body that declares one of those types and then fails to parse is an error, not a quiet empty bind — see ErrMalformedBody.

When to use json: instead

json: exists for the case where a type is already tagged for encoding/json and you would rather not tag it twice:

type Account struct {
	ID    string `json:"id"`
	Email string `json:"email"`
	Hash  string `json:"-"`
}

binder follows encoding/json's naming where it matters for a shared type: a json:"-" field is never bound, and an empty name, as in json:",omitempty", means the Go field name. Unlike encoding/json, names match exactly rather than case-insensitively, and binder reads its own options from the tag, so json:"x,omitempty" also skips an empty value when binding.

binder reads its options from whichever tag it uses, so json:"email,required" means writing an option encoding/json does not define. Nothing breaks, but linters such as staticcheck will flag it as an unknown tag option. Prefer body: for any field that needs binder options, and keep json: for tags shared with serialisation. Do not put both on one field — body would win and the json tag would be dead weight.

Cookies

type Session struct {
	Token string `cookie:"session"`
	Theme string `cookie:"theme,omitempty"`
}

A cookie that is not present binds nothing and leaves the field as it was, unless the field is tagged required.

Headers

Header names are matched case-insensitively, so the tag may spell one however it reads best:

type Request struct {
	Auth    string   `header:"Authorization"`
	TraceID string   `header:"x-request-id"`
	Accept  []string `header:"Accept"`
}

Form body or query: form

form: reads a value the way net/http's r.FormValue does, and as Gin's form tag does: from a form body when the body has the key, and otherwise from the query string. One field serves an HTML form post and a GET ?page=2 alike:

type ListOrders struct {
	Page   int               `form:"page"`   // body page=2, or ?page=2
	Status []string          `form:"status"` // every value, from whichever has the key
	Filter map[string]string `form:"filter"` // filter[team]=core, from either
}

Multipart forms and file uploads

A multipart/form-data body binds its text parts like any other body field, and its file parts to *multipart.FileHeader:

type UploadRequest struct {
	Name   string                  `body:"name"`
	Avatar *multipart.FileHeader   `body:"avatar"`
	Docs   []*multipart.FileHeader `body:"docs"`
}

f, err := req.Avatar.Open()

A field given one file binds a one-element slice; a field declared as a single file takes the first part sent. Binding a file part to anything other than a FileHeader is refused rather than coerced.

An upload counts against the body size limit like any other body, and is held in memory rather than written to a temporary file. Raise the limit deliberately on an upload endpoint, with BindOptions{MaxBodySize: 32 << 20} or whatever it needs — without a bound, such an endpoint is the easiest way to exhaust a server's memory.

Repeated values

A query parameter, header or form field given more than once binds every value when the destination is a slice, and its first value otherwise:

type Request struct {
	Tags []string `query:"tags"` // ?tags=a&tags=b  ->  ["a", "b"]
	Sort string   `query:"sort"` // ?sort=a&sort=b  ->  "a"
}

A single value still binds as a one-element slice, so a handler never has to special-case arity, and into a slice of slices as a list of one list; only a recursive type such as type P []P refuses one. Values are never split on commas: ?tags=a,b is one value, the string "a,b". If you want comma-separated input, split it yourself after binding, or bind into a type that unmarshals text.

Repeated values filling slices is new in 1.1.0. Before that a repeated query parameter bound only its first value, whatever the field's type.

Maps

A map field binds from a JSON object, and from a query string or form body written as name[key]=value pairs, the style OpenAPI calls deepObject:

type Search struct {
	Filter map[string]string   `query:"filter"` // ?filter[status]=open&filter[team]=core
	Min    map[string]int      `query:"min"`    // ?min[price]=10
	Flags  map[string]bool     `query:"flag"`   // ?flag[draft]=false
	Tags   map[string][]string `query:"tags"`   // ?tags[any]=a&tags[any]=b
}

Keys convert to the map's key type — any type that converts from text, such as a string, a number, a bool or a TextUnmarshaler — and values as a field of the element type would, so map[string]time.Time or map[string]uuid.UUID work too. Clients usually percent-encode the brackets, as filter%5Bstatus%5D=open, which reads the same. In a query or form a repeated key fills a slice value and otherwise binds its first value; in a JSON object the last occurrence binds, as for any member. In a query or form an empty value counts as absent, and a map with no entries is left nil, so required reports it; a JSON object binds as sent, so {} is an empty map that satisfies required, and a null value gives its key the zero value, as in encoding/json.

A bad entry is reported under the name the client sent, such as min[price]. Only one level of brackets is read: filter[a][b] has no agreed meaning. The map is replaced, not merged into. In a multipart form, file parts named this way bind into a map[string]*multipart.FileHeader and are refused by any other map. With DisallowUnknownFields, a form field such as meta[a] counts as known when a map field is named meta.