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.

TagReadsNotes
path:"id"Path parametersVia Request.PathValue, so the router must have declared {id}.
query:"page"URL query stringThe query is parsed once per call, and only if some field asks for it.
body:"email"Request bodyJSON or form-encoded, 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.

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"`
}
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 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.

An upload counts against 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.

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