binder / Docs / Types & conversion

Types & conversion

Every source except a JSON body hands binder a string; a JSON body hands it whatever encoding/json decoded. 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. Non-string JSON values are formatted, so a JSON number bound to a string field gives its text.
int, int8int64Parsed base 10 from a string; taken directly from a JSON number. 1e5 and 1.0 go through float and truncate.
uint, uint8uint64As above, and a negative value is an error rather than a wrap-around.
float32, float64Parsed with strconv.ParseFloat, or taken from a JSON number.
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. See Slices.
*TAllocated if nil, then filled as T. A missing value leaves the pointer nil, which is how you tell “absent” from “sent as zero”.
a structFilled from a nested JSON object. See Nested structs.
anything with UnmarshalTextHanded the raw text before any of the above. See Custom types.
[N]TNot supported. Fixed-size arrays are an error; use a slice.
map, chan, funcNot supported. Reported as an unsupported type.

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.

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. Elements are converted individually, and an element that will not convert reports its index.

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, and a *Address is allocated when the key is present. A key absent from the object leaves its field untouched.

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

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 object or array bound to such a field is an error rather than a re-encoded string.