Binding values
How input keys resolve to struct fields through tag priority, nesting, embedding, slices, and coercion.
Set walks the struct field by field and, for each field, looks for a matching key in the input map. How it matches and what it does with the value is the whole binding model. This guide covers every part of it.
Tag priority
A field can carry several tags. The tag priority list decides which of a field's tags is used to look it up, and the first tag in the list that the field actually carries wins. The default order is json then yaml, so a field tagged json:"host" yaml:"host" is looked up under host.
Change the order with WithTags. This is how you point the same struct at a different source vocabulary without re-tagging it.
// look up by the "arg" tag first, then "short", then fall back to json.
manager := structs.New(cfg, structs.WithTags("arg", "short", "json"))Note
WithTags sets the order of tags used to look a field up. It does not restrict which struct tags may exist. A field with no tag in the priority list falls back to its Go field name.
Type coercion
Inputs are loosely typed on purpose. A value arrives as whatever your source produced, usually a string from an env var or a flat config value, and structs coerces it into the field's declared type. A port given as the string "9090" lands in an int field, "true" lands in a bool, and so on across string, int, float, bool, slice, map, and interface fields.
type Config struct {
Port int `json:"port"`
Debug bool `json:"debug"`
}
inputs := map[string]any{
"port": "9090", // string -> int
"debug": "true", // string -> bool
}
// after Set: Port == 9090, Debug == trueThe coercion helpers are also exported directly if you need them outside a struct. See the utilities reference.
Nested structs
A named struct field nests. Its inner fields are reachable three equivalent ways, so a decoded config, a flat env map, or a dotted override all drop straight in.
type Server struct {
Database Database `json:"database" env:"DATABASE"`
}
type Database struct {
URL string `json:"url" env:"URL"`
}// 1. dotted path: the field's tag glued to its parent's with "."
map[string]any{"database.url": "mysql://127.0.0.1:3306/beep"}
// 2. nested map: a sub-section keyed by the parent's tag
map[string]any{"database": map[string]any{"url": "mysql://127.0.0.1:3306/beep"}}
// 3. env tag: the env tags glued with "_"
map[string]any{"DATABASE_URL": "mysql://127.0.0.1:3306/beep"}Nesting goes arbitrarily deep (a.b.c, or maps within maps), which is how a decoded JSON or YAML config drops in without reshaping.
Warning
Nested maps must be map[string]any at every level, the shape JSON and YAML decoders produce. A typed intermediate map such as map[string]map[string]any is only descended into where its element type is map[string]any. A deeper typed-map intermediate is not traversed and the leaf stays unset. Use the dotted path or a map[string]any sub-section instead.
Embedded structs
An untagged embedded struct has its fields promoted to the parent level, exactly as Go and encoding/json promote them, with no wrapper and no prefix. The embedded type may be exported or unexported.
type Network struct {
Host string `json:"host" env:"HOST"`
Port int `json:"port" env:"PORT"`
}
type Server struct {
Network // embedded: Host and Port are promoted
Name string `json:"name"`
}Set the promoted fields by their own tag or name, with no parent prefix:
map[string]any{
"host": "127.0.0.1", // -> Server.Host
"port": 8080, // -> Server.Port
"name": "edge", // -> Server.Name
}A tagged anonymous field is not promoted. It nests under its tag instead, just like encoding/json, so it behaves like the named nesting above.
Slices
A single string handed to a scalar slice field is split into elements and each element is coerced. The separator is a comma by default, or set your own per field with the sep tag.
type Config struct {
Tags []string `json:"tags" sep:","`
}
map[string]any{"tags": "edge,beta,canary"} // -> []string{"edge", "beta", "canary"}Already-structured input passes through untouched, so a real []string in the map is used as-is. Set sep:"" to turn splitting off and keep each value exactly as given.
When a key is supplied more than once, collect the raw strings into a structs.MultiValue rather than a plain string. Each element is split on the field's separator and the pieces are joined into one slice, so MultiValue{"a,b", "c"} yields ["a", "b", "c"]. This is the shape a source produces when the same key appears repeatedly, such as a repeated command-line flag whose values you gather before handing the map to structs.
type Config struct {
Tags []string `json:"tags" sep:","`
}
map[string]any{"tags": structs.MultiValue{"a,b", "c"}} // -> []string{"a", "b", "c"}