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 failure is a
BindErrors entry naming the field, wrapping
ErrMissingRequired, and errors.Is finds it
through the list:
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 — an empty value counts as none, so ?q= is treated as no q. For a slice, every value given must be empty. |
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. |
Every missing required field is reported, along with any other field that failed, in one error. See Error handling for turning that into a response.
omitempty
Skip binding when the value is present but empty, leaving the field at
whatever it already held. A value that is absent never touches the field,
with or without the option, so defaults work by setting the field before
binding. omitempty extends that to a body value that is
present but empty; for a nested struct, an empty {} is
skipped whole, so the required fields inside it are not
checked:
type UpdateProfile struct {
Name string `body:"name,omitempty"`
Limit int `body:"limit,omitempty"`
}
p := UpdateProfile{Name: current.Name, Limit: current.Limit}
if err := binder.Bind(r, &p); err != nil {
// ...
}
// {"name":"","limit":0} leaves both at their current values.
What counts as empty depends on the value. A JSON body value is empty
when it is an empty string, a zero number, false, null, or an empty array
or object. Every other source carries text, so there only the empty string
is empty: ?limit=0 binds 0. On path,
query and header the option changes nothing,
since an empty value there already counts as absent.
On a pointer field omitempty has no effect. The pointer
already tells a value that was not sent (nil) from one sent as zero, so
{"active": false} sets a *bool to false, as a
PATCH needs.
required and omitempty on the same field are
not contradictory for a body or cookie value: the key must be present, and
if it is present but empty the field is left alone. On path,
query and header an empty value is missing, so
required reports it.
query is new in
1.1.0. Before that,
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 |
The largest body, in bytes, this call reads. Zero applies DefaultMaxBodySize (10 MB); a negative value removes the limit. |
DisallowUnknownFields | false |
Reports each top-level body key that no field of the target binds, as a BindErrors entry wrapping ErrUnknownField. 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.
Each unknown key is its own entry, after the fields that failed, with
Name set to the key and Field empty, since no
field binds it. They are reported alongside any other failures rather
than ending binding, and errors.Is still finds them:
if errors.Is(err, binder.ErrUnknownField) {
// at least one key in the body was not recognised
}Body size limits
Bind caps bodies at 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.
To use a different limit, pass it per call:
// An upload endpoint that accepts up to 32 MB.
err := binder.BindWithOptions(r, &req, binder.BindOptions{MaxBodySize: 32 << 20})
There is no package-level setting to change: each endpoint states its
own limit, and nothing one part of a program sets can change another's.
A MaxBodySize of zero applies the default, so setting only
DisallowUnknownFields never removes the cap by accident. A
negative value removes the limit, which is not recommended for an
endpoint reachable from the internet.
DefaultMaxBodySize is not part of the
compatibility contract — pass MaxBodySize explicitly if your
service depends on a particular limit. See
Compatibility.