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.
| Tag | Reads | Notes |
|---|---|---|
path:"id" | Path parameters | Via Request.PathValue, so the router must have declared {id}. |
query:"page" | URL query string | Read 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 body | JSON, form-encoded or multipart, chosen by Content-Type. |
json:"email" | Request body | For structs whose tags are shared with serialisation. |
cookie:"session" | Request cookies | Name matched exactly, as cookies are case-sensitive. |
header:"X-Request-ID" | Request headers | Matched case-insensitively. |
form:"page" | Form body, then query string | As 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.
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
}- A form body is
application/x-www-form-urlencodedormultipart/form-data, file parts included. A JSON body is not a form, so on a JSON requestform:reads the query alone, and a JSON member of the same name is not bound by it. - Values come from one source or the other, never both: a slice or map takes the body's values when the body has any, and otherwise the query's.
requiredandomitemptyfollow the rules of the source the value came from: a key present but empty in a form body satisfiesrequired, and an empty query value counts as missing.- With
DisallowUnknownFields, aform:key is known in a form body; in a JSON body it is not. form:has its own keys: abody:"x"field and aform:"x"field both bind.
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.
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.
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.