binder / Docs / Options

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:

SourceMissing when
path, query, headerAbsent 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, jsonThe key is absent. A key present with an empty value satisfies required.
cookieNo 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.

Tag options binding on every source but 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 {
	// ...
}
FieldDefaultEffect
MaxBodySize0 The largest body, in bytes, this call reads. Zero applies DefaultMaxBodySize (10 MB); a negative value removes the limit.
DisallowUnknownFieldsfalse 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.

The value of DefaultMaxBodySize is not part of the compatibility contract — pass MaxBodySize explicitly if your service depends on a particular limit. See Compatibility.