DocsStructsBinding values

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 == true

The 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"}