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 type | Behaviour |
|---|---|
string | Taken as-is. Non-string JSON values are formatted, so a JSON number bound to a string field gives its text. |
int, int8 … int64 | Parsed base 10 from a string; taken directly from a JSON number. 1e5 and 1.0 go through float and truncate. |
uint, uint8 … uint64 | As above, and a negative value is an error rather than a wrap-around. |
float32, float64 | Parsed with strconv.ParseFloat, or taken from a JSON number. |
bool | strconv.ParseBool, so 1, t, T, true, TRUE and their false counterparts. A JSON number is true when non-zero. |
[]T | Each element converted as T. See Slices. |
*T | Allocated if nil, then filled as T. A missing value leaves the pointer nil, which is how you tell “absent” from “sent as zero”. |
a struct | Filled from a nested JSON object. See Nested structs. |
anything with UnmarshalText | Handed the raw text before any of the above. See Custom types. |
[N]T | Not supported. Fixed-size arrays are an error; use a slice. |
map, chan, func | Not 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.
[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.
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.
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.