Options
Two options ride on the tag, and two more are passed per call. Between them they cover the questions binding has to answer: what happens when a value is missing, when it is empty, when the body is enormous, and when the body carries keys the struct never asked for.
Tag options
An option follows the key, comma-separated, in whichever tag binder reads for that field:
type Request struct {
Email string `body:"email,required"`
Nick string `body:"nick,omitempty"`
}required
Binding fails if the value is missing from its source. The error is a
*BindError naming the field, wrapping
ErrMissingRequired:
if errors.Is(err, binder.ErrMissingRequired) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}What counts as missing depends on the source, because the sources differ:
| Source | Missing when |
|---|---|
path, query, header | Absent or empty — neither source distinguishes the two, so ?q= is treated as no q. |
body, json | The key is absent. A key present with an empty value satisfies required. |
cookie | No cookie of that name was sent. A cookie with an empty value satisfies required. |
Binding stops at the first missing required field. If you need every
problem in one response rather than the first, collect them in a
Validate method instead —
binder deliberately does not accumulate.
omitempty
Skip binding when the value is present but empty, leaving the field at whatever it already held. That is what makes defaults work: set the field before binding, and an empty input will not overwrite it.
type ListOptions struct {
PerPage int `query:"per_page,omitempty"`
Sort string `query:"sort,omitempty"`
}
opts := ListOptions{PerPage: 25, Sort: "created"}
if err := binder.Bind(r, &opts); err != nil {
// ...
}
// ?per_page= leaves PerPage at 25 rather than zeroing it.
Empty means the empty string, a zero number, false, or an empty slice or
map. required and omitempty on the same field
are not contradictory: the value must be present, and if it is present but
empty the field is left alone.
body:"email,omitempty" looked for a key literally named
email,omitempty and so never bound anything. Fields written
that way will start receiving values on upgrade.
Per-call options
BindWithOptions is Bind with configuration. The
zero BindOptions behaves exactly as Bind does,
so only the fields you care about need setting:
opts := binder.BindOptions{
MaxBodySize: 1 << 20, // 1 MB for this call only
DisallowUnknownFields: true, // reject body keys nothing binds
}
if err := binder.BindWithOptions(r, &req, opts); err != nil {
// ...
}| Field | Default | Effect |
|---|---|---|
MaxBodySize | 0 |
Overrides the package-level binder.MaxBodySize for this call. Zero leaves the package setting in force; a negative value removes the limit for this call alone. |
DisallowUnknownFields | false |
Fails with ErrUnknownField when the body carries a top-level key that no field of the target binds. Keys nested inside objects are not inspected. |
Rejecting unknown fields
Useful on write endpoints, where a client sending emial
would otherwise get a cheerful 200 and no email change. It is a per-call
option rather than the default because a rejected typo is the right
answer for an internal API and the wrong one for a public endpoint that
has to tolerate clients sending more than it reads.
if errors.Is(err, binder.ErrUnknownField) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}Body size limits
Bodies are capped at binder.MaxBodySize, which defaults to
DefaultMaxBodySize — 10 MB. An oversized body is rejected
with ErrBodyTooLarge rather than truncated, so a request is
never bound from half a body. The limit is enforced while reading rather
than trusted from Content-Length, which the client controls
and may understate.
Set it once during initialisation to change the default everywhere:
func init() {
binder.MaxBodySize = 2 << 20 // 2 MB
}Zero or less removes the limit entirely and restores the unbounded behaviour of 1.0. The variable is read on every call, so set it at startup rather than while requests are in flight.