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 type | Behaviour |
|---|---|
string | Taken 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 … int64 | Parsed 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 … 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. |
time.Duration | Text 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. |
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. 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. |
*T | Allocated 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 struct | Filled from a nested JSON object. See Nested structs. |
anything with UnmarshalText | Handed 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 UnmarshalJSON | Decodes a JSON body value itself, as with encoding/json. See JSON decoding. |
[N]T | Not supported. Fixed-size arrays are an error; use a slice. |
map[K]V | Filled 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. |
any | Takes the value as decoded, as encoding/json does, with numbers as json.Number so large integers keep their precision. |
chan, func, an interface with methods | Not 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].
[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.
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.
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.