binder / Docs / Types & conversion

Types & conversion

Every source except a JSON body hands binder text; a JSON body hands it a JSON value: a string, a number, true or false, an array or an object. Either way the value is converted to the field's type, and a value that will not convert is an error naming the field rather than a zero value quietly left in place.

What binds

Field typeBehaviour
stringTaken as-is. A JSON number or bool bound to a string field gives its text; a JSON array or object is an error.
int, int8 … int64Parsed base 10 from a string; taken directly from a JSON number. In a JSON number 1e5 and 1.0 are whole numbers and bind, though as text only plain digits do; a fraction such as 1.9 is an error rather than truncated.
uint, uint8 … uint64As above, and a negative value is an error rather than a wrap-around.
float32, float64Parsed with strconv.ParseFloat, or taken from a JSON number.
time.DurationText such as 5s, 1m30s or 250ms, from any source including a JSON string, is parsed with time.ParseDuration; a number without a unit is an error, except 0. A JSON number is nanoseconds, as encoding/json treats it.
boolstrconv.ParseBool, so 1, t, T, true, TRUE and their false counterparts. A JSON number is true when non-zero.
[]TEach element converted as T. A JSON string into a plain []byte, behind pointers too, is base64, as in encoding/json; text from a query, header or form is taken as its bytes. See Slices.
*TAllocated if nil, then filled as T. A missing value, or a JSON null, leaves the pointer as it was (nil unless set before binding, or by an earlier occurrence of the member), which is how you tell “absent” from “sent as zero”. A value that fails to bind leaves a nil pointer nil as well.
a structFilled from a nested JSON object. See Nested structs.
anything with UnmarshalTextHanded the raw text before any of the above (a JSON value other than a string goes to UnmarshalJSON instead when the type has both). See Custom types.
anything with UnmarshalJSONDecodes a JSON body value itself, as with encoding/json. See JSON decoding.
[N]TNot supported. Fixed-size arrays are an error; use a slice.
map[K]VFilled from a JSON object, or from name[key]=value pairs in a query or form. Keys convert to K, any type that converts from text: a string, a number, a bool or a TextUnmarshaler; values convert as a V field would. A bad entry is reported by key, as meta[a]. See Maps.
anyTakes the value as decoded, as encoding/json does, with numbers as json.Number so large integers keep their precision.
chan, func, an interface with methodsNot supported. Reported as an unsupported type, or as a mismatch for an array or object.

Strings in, typed values out

A query string has no types. ?page=2&draft=true is two strings, and it is the field that decides what they mean:

type List struct {
	Page  int     `query:"page"`  // "2"    -> 2
	Draft bool    `query:"draft"` // "true" -> true
	Ratio float64 `query:"ratio"` // "0.75" -> 0.75
}

Send ?page=two and binding fails with a BindError naming Page, the source query and the key page — enough to write a useful 400 without guessing. Send ?page=two&draft=maybe and both come back, in one BindErrors.

A value the field cannot hold is refused

Conversion is checked against the destination's range, so a number too large for its field fails rather than wrapping:

type Request struct {
	Small int8 `query:"n"` // ?n=9999 -> error, not 15
}

Reflection truncates on assignment, so without the check 9999 would bind silently as 15. The same guard applies to unsigned and floating-point fields, and to every source.

Slices

A slice fills from either shape of repeated input:

type Request struct {
	// ?tag=go&tag=http, or a repeated form field
	Tags []string `query:"tag"`

	// {"scores": [1, 2, 3]} in a JSON body
	Scores []int `body:"scores"`

	// A JSON array of objects binds element by element
	Items []Item `body:"items"`
}

A single value binds as a one-element slice, so a handler never has to special-case arity, and into a slice of slices as a list of one list; only a recursive type such as type P []P refuses one. Elements are converted individually, and each element that will not convert is reported with its index, as tag[1].

Fixed-size arrays are rejected on purpose. [3]string asks the request to supply exactly three values and gives binder no good answer when it supplies two, so the library declines rather than pick one. Use []string and check the length yourself.

Pointers

A pointer field is the way to distinguish a value that was not sent from one that was sent as zero — the difference between “leave this alone” and “set it to 0” in a PATCH handler:

type PatchSettings struct {
	// nil  -> the client did not mention retries
	// &0   -> the client asked for zero retries
	Retries *int `body:"retries"`
}

Nested structs

A struct field binds from a nested JSON object. Its own fields are matched on their body tag, falling back to json — the other four sources have no nesting to read, so they are not consulted inside a nested struct:

type Address struct {
	Street string `body:"street"`
	City   string `body:"city"`
}

type User struct {
	Name    string  `body:"name"`
	Address Address `body:"address"`
}
{"name": "ada", "address": {"street": "1 Main St", "city": "London"}}

Nesting works to any depth the JSON parser accepts, 10,000 levels, and only fields with a body or json tag bind, as at the top level. A *Address is allocated when the key is present with a value other than null. A key absent from the object leaves its field untouched. A value inside that will not convert is reported with its full path, such as address.city, alongside any other field that failed.

Form bodies are flat, so nested structs only fill from JSON. A form field literally named address cannot describe an object.

Embedded structs

An embedded struct with no tag of its own has its fields promoted, as in encoding/json, so request types can share a set of parameters:

type Paging struct {
	Page  int `query:"page"`
	Limit int `query:"limit"`
}

type ListOrders struct {
	Paging        // ?page=2&limit=20 fills Page and Limit
	*Audit        // allocated only if one of its fields is sent (a null is not)
	Status string `query:"status"`
}

Promoted fields bind from every source and take every option. Inside a nested struct, which binds from a JSON object, only their body and json tags are read. A failure names the field by its path, such as Paging.Limit. An outer field shadows a promoted one with the same key, body: and json: counting as the same key, and at the same depth the first declared wins. An embedded struct tagged with a name, such as Audit `body:"audit"`, is an ordinary nested object; one tagged `json:"-"` is left out entirely.

Custom types

Any type implementing encoding.TextUnmarshaler is handed the raw text and decides for itself. This is the extension point: it covers enums, identifiers, dates in a house format and anything else a string needs to become.

type Status string

const (
	StatusOpen   Status = "open"
	StatusClosed Status = "closed"
)

func (s *Status) UnmarshalText(text []byte) error {
	switch Status(text) {
	case StatusOpen, StatusClosed:
		*s = Status(text)
		return nil
	default:
		return fmt.Errorf("unknown status %q", text)
	}
}

type Filter struct {
	Status Status `query:"status"`
}

The method is looked for on the field's type and on a pointer to it, so the usual pointer receiver works on a value field. Standard library types that already implement it — time.Time, net.IP, netip.Addr, uuid.UUID — bind with no code of your own:

type Query struct {
	Since  time.Time `query:"since"`  // RFC 3339
	Client net.IP    `header:"X-Real-IP"`
	Trace  uuid.UUID `header:"X-Trace-Id"`
}

An error returned from UnmarshalText is wrapped in a BindError and reachable with errors.Is and errors.As, so a custom sentinel survives the trip out to the handler.

A TextUnmarshaler only receives text. In a JSON body it is used when the value is a string; a JSON number, bool, object or array bound to a type that has only UnmarshalText is an error rather than a re-encoded string.

JSON decoding

A type with its own JSON decoding — UnmarshalJSON, or json/v2's UnmarshalJSONFrom — decodes itself from a JSON body, as with encoding/json. That covers money and decimal types, enums that arrive as numbers, and json.RawMessage:

type Order struct {
	Total   Money           `body:"total"`   // Money has UnmarshalJSON
	Payload json.RawMessage `body:"payload"` // kept as the client sent it
}

A JSON body value is handed over exactly as sent, at any depth; with omitempty, an empty one is skipped instead. A JSON null sets nothing. A JSON string goes to UnmarshalText when the type has that too, so a value reads the same from a body as from a query string, and a type with only UnmarshalJSON is given text from other sources as a JSON string. An error the method returns is reported against the field like any other.