structdefaults

package module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 27, 2026 License: MIT Imports: 8 Imported by: 0

README

koanf-structdefaults

A koanf provider that reads koanf-default:"…" struct tags and emits a nested map[string]any of parsed defaults. Load it as the lowest-priority layer and let file, env, and flag providers override naturally.

Zero production dependencies. Distributed as a standalone Go module.

Why

The conventional way to declare defaults in a koanf-based app is a hand-built confmap.Provider:

k.Load(confmap.Provider(map[string]any{
    "server.port":    8080,
    "server.host":    "localhost",
    "server.timeout": "30s",
}, "."), nil)

That works, but the defaults drift away from the struct, lose type-safety (any everywhere), and duplicate every field name. This provider lets defaults live next to the field they describe:

type Config struct {
    Server struct {
        Host    string        `koanf:"host"    koanf-default:"localhost"`
        Port    int           `koanf:"port"    koanf-default:"8080"`
        Timeout time.Duration `koanf:"timeout" koanf-default:"30s"`
    } `koanf:"server"`
    LogLevel string `koanf:"log_level" koanf-default:"info"`
}

Install

go get github.com/uded/koanf-structdefaults

Requires Go 1.23+.

Usage

package main

import (
    "log"
    "time"

    "github.com/knadh/koanf/parsers/yaml"
    "github.com/knadh/koanf/providers/env"
    "github.com/knadh/koanf/providers/file"
    "github.com/knadh/koanf/v2"

    "github.com/uded/koanf-structdefaults"
)

type Config struct {
    Server struct {
        Host    string        `koanf:"host"    koanf-default:"localhost"`
        Port    int           `koanf:"port"    koanf-default:"${PORT:-8080}"`
        Timeout time.Duration `koanf:"timeout" koanf-default:"30s"`
    } `koanf:"server"`
    LogLevel string `koanf:"log_level" koanf-default:"info"`
}

func main() {
    k := koanf.New(".")

    // Layer 1 (lowest priority): declared defaults.
    p, err := structdefaults.New(&Config{}, structdefaults.Options{Delim: "."})
    if err != nil {
        log.Fatal(err)
    }
    if err := k.Load(p, nil); err != nil {
        log.Fatal(err)
    }
    // Layer 2: file overrides defaults.
    _ = k.Load(file.Provider("config.yaml"), yaml.Parser())
    // Layer 3 (highest): env overrides everything.
    _ = k.Load(env.Provider("APP_", ".", nil), nil)

    var cfg Config
    if err := k.Unmarshal("", &cfg); err != nil {
        log.Fatal(err)
    }
}

Options

type Options struct {
    Delim      string    // required ("." for typical configs)
    PathTag    string    // default: "koanf"
    DefaultTag string    // default: "koanf-default"
    Lookup     EnvLookup // default: os.LookupEnv
    Strict     bool      // default: false
}
Field Effect
Delim Path separator. Empty value → ErrInvalidConfig from New.
PathTag / DefaultTag Override the tag names used to name fields and read defaults. Empty → library default.
Lookup Custom env-var resolver. Pass nil to use os.LookupEnv. Useful for hermetic tests, secret stores, or precedence layering.
Strict When true, New walks the entire struct eagerly and surfaces any error at construction time rather than at the first Read.

Supported field types

Type Notes
string, bool parsed via strconv
int, int8/16/32/64 parsed via strconv.ParseInt with the matching bit size
uint, uint8/16/32/64 parsed via strconv.ParseUint
float32, float64 parsed via strconv.ParseFloat
time.Duration parsed via time.ParseDuration (e.g. "30s", "1h15m")
Any encoding.TextUnmarshaler both value-receiver and pointer-receiver
Nested struct walked recursively
Pointer-to-struct walked via a temporary instance — your input struct is never mutated
Anonymous embedded struct flattened (squash) unless it carries an explicit koanf:"name" tag

Tag semantics

Tag Behavior
koanf:"name" path segment is name
koanf:"-" field is skipped entirely (overrides any koanf-default)
koanf:"" or absent path segment is the Go field name
koanf-default:"value" declared default for this field
koanf-default:"" empty-string default (only meaningful for string)
koanf-default: absent no entry emitted (output is sparse)

Environment variable substitution

Tag values may include POSIX-style ${VAR} and ${VAR:-fallback} references. Substitution runs before type parsing, so it works for any field type:

type Config struct {
    Host    string        `koanf:"host"    koanf-default:"${HOST:-localhost}"`
    Port    int           `koanf:"port"    koanf-default:"${PORT:-8080}"`
    Timeout time.Duration `koanf:"timeout" koanf-default:"${TIMEOUT:-30s}"`
    Region  string        `koanf:"region"  koanf-default:"${REGION}"` // no fallback
}
Form Behavior
${VAR} Resolves VAR. If unset, returns ErrUnsetEnv.
${VAR:-fallback} Uses VAR if set; otherwise the literal fallback (which may be empty: ${VAR:-}).
$$ … literal $ Not yet supported — request via issue if needed.

Substitution is single-pass and non-recursive: env-var values are not re-scanned for ${...}. This is intentional — prevents indirect env-var resolution.

Custom lookup

For hermetic tests or secret stores (Vault, AWS Secrets Manager), pass a custom Lookup:

p, err := structdefaults.New(&cfg, structdefaults.Options{
    Delim: ".",
    Lookup: func(name string) (string, bool) {
        return vaultClient.Get(name)
    },
})
Security note

Struct-tag values are compiled into your binary and visible in strings ./binary. Never embed secrets directly in koanf-default:"…" — use ${VAR} substitution and resolve them from your environment or secret store at runtime. Errors from this library may include env-var names (not values); route library errors through your standard log-redaction pipeline.

Strict mode

By default, parse errors and unset env vars surface from Read() at koanf load time. Set Strict: true to force eager validation at construction:

p, err := structdefaults.New(&cfg, structdefaults.Options{
    Delim:  ".",
    Strict: true,
})
// err is non-nil if any default fails to parse, any env var is unset
// without a fallback, or the struct contains a cyclic type.

Strict is a startup-time correctness gate: catches typos in tag values before the app starts serving traffic.

Errors

All errors are sentinel-wrapped via %w; match with errors.Is or errors.As:

Sentinel When
ErrInvalidConfig Options.Delim is empty (returned from New).
ErrInvalidInput target is nil, a non-struct, or a nil pointer-to-struct (returned from New).
ErrCyclicType walker encountered a struct type that recursively references itself.
ErrInvalidValue a tag value cannot be parsed for an otherwise-supported type (e.g. koanf-default:"8O8O" on int). Use errors.As to recover the underlying *strconv.NumError etc.
ErrUnsupportedType the field's Go type cannot carry a default at all (slice, map, channel, func). Programmer error: fix the struct.
ErrUnsetEnv ${VAR} reference with no fallback and the env var is unset.
ErrUnsupported returned from ReadBytes() (this provider is Read()-only).

Output shape

Read() returns a nested map[string]any whose tree shape mirrors the koanf path layout:

map[string]any{
    "server": map[string]any{
        "host":    "localhost",
        "port":    8080,
        "timeout": time.Duration(30 * time.Second),
    },
    "log_level": "info",
}

This matches what every other koanf provider emits and merges correctly with overrides from file, env, and flag layers.

What it doesn't do

  • Slice / map defaults. Comma-split syntax has too many escaping pitfalls; for the rare slice/map default, build it via confmap or initialize the field directly. A future koanf-default-json:"[1,2,3]" tag is on the roadmap.
  • Validation (required:"…"). Different concern; out of scope.
  • Mutating your input struct. Read-only by design.

License

MIT — see LICENSE.

Documentation

Overview

Package structdefaults provides a koanf provider that reads koanf-default struct tags and emits a nested map[string]any of default values whose tree shape mirrors the koanf path layout. Load it as the first (lowest-priority) layer so that file, env, and flag providers override it naturally.

Index

Constants

This section is empty.

Variables

View Source
var ErrCyclicType = errors.New("structdefaults: cyclic struct type")

ErrCyclicType is returned when the walker encounters a struct type that recursively references itself (directly or transitively). Loading defaults for such a type would cause unbounded recursion at startup.

View Source
var ErrInvalidConfig = errors.New("structdefaults: invalid Options")

ErrInvalidConfig is returned by New when the Options struct is invalid — e.g. Delim is empty. This indicates a programmer error in the call site.

View Source
var ErrInvalidInput = errors.New("structdefaults: input must be a non-nil pointer to a struct")

ErrInvalidInput is returned when the target passed to New is nil, a non-struct, or a nil pointer-to-struct.

View Source
var ErrInvalidValue = errors.New("structdefaults: invalid default value")

ErrInvalidValue is returned when a field's type IS supported but the tag value (or its post-substitution form) cannot be parsed — e.g. a malformed integer, a bad duration string, or a TextUnmarshaler that rejected the input. This is typically an operator/config error: fix the tag value or the env var feeding it. Use errors.As to recover the underlying parse error (*strconv.NumError, etc.).

View Source
var ErrUnsetEnv = errors.New("structdefaults: env var unset with no fallback")

ErrUnsetEnv is returned when a default value contains ${VAR} and the referenced environment variable is unset with no fallback provided. Use ${VAR:-} to opt into an empty-string fallback.

View Source
var ErrUnsupported = errors.New("structdefaults: ReadBytes is not supported")

ErrUnsupported is returned by ReadBytes because the structdefaults provider operates on in-memory structs, not byte streams.

View Source
var ErrUnsupportedType = errors.New("structdefaults: unsupported field type")

ErrUnsupportedType is returned when a field's Go type cannot carry a koanf-default value at all — e.g. slice, map, channel, or function fields with a default tag. This is a programmer error: fix the struct definition.

Functions

This section is empty.

Types

type EnvLookup

type EnvLookup func(name string) (string, bool)

EnvLookup resolves an environment variable name to its value. Implementations must return (value, true) when the variable is set (even to an empty string) and ("", false) when it is unset. The default lookup is os.LookupEnv.

Implementations must be safe for concurrent use; the provider holds a reference and may call it from any goroutine that triggers a Read.

type Options

type Options struct {
	// Delim is the path separator used both to interpret the path tag values
	// and to nest entries in the output map. Required; empty Delim returns
	// ErrInvalidConfig from New.
	Delim string

	// PathTag is the struct tag whose value names the config path segment for
	// each field. Defaults to "koanf".
	PathTag string

	// DefaultTag is the struct tag whose value declares the field's default.
	// Defaults to "koanf-default".
	DefaultTag string

	// Lookup resolves ${VAR} references found in DefaultTag values. Defaults
	// to os.LookupEnv. Pass a custom function for hermetic tests, secret
	// stores (Vault, AWS Secrets Manager), or precedence layering.
	Lookup EnvLookup

	// Strict, when true, eagerly walks the entire struct at construction time
	// and surfaces any error (cyclic types, parse failures, unset env vars
	// without fallback) from New rather than waiting for the first Read call.
	Strict bool
}

Options configures a StructDefaults provider. All fields are optional except Delim. Zero values trigger sensible defaults documented per field.

type StructDefaults

type StructDefaults struct {
	// contains filtered or unexported fields
}

StructDefaults walks struct tags to produce a nested map of defaults suitable for koanf.Load. It is immutable after construction; safe for concurrent Read calls.

func New

func New(target any, opts Options) (*StructDefaults, error)

New constructs a StructDefaults provider. It validates Options and the target struct shape, applying defaults for any zero-valued option fields. When Options.Strict is true, it additionally performs a full walk of the target struct so that any default-parsing or env-substitution errors surface immediately rather than at the first Read call.

Returns ErrInvalidConfig if Options.Delim is empty, ErrInvalidInput if target is not a non-nil pointer to a struct, or any error produced by an eager Strict-mode walk (ErrCyclicType, ErrInvalidValue, ErrUnsetEnv, ErrUnsupportedType).

func (*StructDefaults) Read

func (p *StructDefaults) Read() (map[string]any, error)

Read walks the struct tags and returns a nested map[string]any whose tree shape mirrors the koanf path layout (split on Options.Delim). Only fields with an explicit DefaultTag contribute entries (sparse output).

func (*StructDefaults) ReadBytes

func (p *StructDefaults) ReadBytes() ([]byte, error)

ReadBytes is not supported. Returns ErrUnsupported.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL