binder / Docs / Binding sources
Binding sources
A field's tag says where its value comes from. There are six sources, and a field may name 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 | The query is parsed once per call, and only if some field asks for it. |
body:"email" | Request body | JSON or form-encoded, 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. |
Precedence runs path, query, body,
json, cookie, header. 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.
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 and query an empty value counts as
missing, because neither source distinguishes “absent” from “present and
empty”: ?q= and no q at all arrive identically.
That matters only in combination with
required.
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. A body whose type is
neither is left unread, and the request binds from its other sources.
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"`
}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 at its
zero value, 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"`
}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.
MaxBodySize
like any other body, and is held in memory rather than written to a
temporary file. Raise the limit deliberately on an upload endpoint —
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. 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.