Files
knadh-koanf/PR.md
T
dualfroz 9f860a7fd4 fix: MapKeys returns empty slice for natively-typed maps
Get() preserves the concrete type of a map[string]T value loaded via
Set() or a provider like structs, but MapKeys() only type-asserted
map[string]any, so it silently reported no keys for anything else.
Read keys via reflection instead so any map with string keys works.
2026-09-04 08:08:58 +02:00

3.1 KiB

MapKeys returns an empty slice for a natively-typed map

Problem

Koanf.MapKeys(path) is documented to return the sorted keys of the map at path, or an empty slice "if the path is not a map". A value at a key path does not have to be map[string]any to be a map, though: Set(), or a provider such as structs, can put a map[string]T (map[string]int64, map[string]bool, ...) directly into the config tree, and Koanf.Get() returns it verbatim with its concrete type intact (koanf.go's Get only special-cases map[string]any for its copy, everything else falls through to copystructure.Copy, which preserves the type).

MapKeys only checked o.(map[string]any), so a natively-typed map failed that assertion and produced []string{} even though the map plainly has keys - a silent contract violation, not a "not a map" case.

k := koanf.New(".")
k.Load(confmap.Provider(map[string]any{
    "ints": map[string]int64{"a": 1, "b": 2},
}, "."), nil)

k.Get("ints")      // map[string]int64{"a":1, "b":2}
k.MapKeys("ints")  // []string{} -- should be []string{"a", "b"}

Root cause

koanf.go:410-429, MapKeys:

mp, ok := o.(map[string]any)
if !ok {
    return out
}

This is the same shape of bug already fixed for Int64Map, Float64Map and BoolMap in #444 (checking only map[string]any and silently dropping the natively-typed sibling), applied to a different getter that PR does not touch.

Fix

Since MapKeys only needs the keys and does not care about the value type, the fix reads them through reflection instead of enumerating every possible map[string]T: any map whose key kind is string is accepted, map[string]any included.

rv := reflect.ValueOf(o)
if rv.Kind() != reflect.Map || rv.Type().Key().Kind() != reflect.String {
    return out
}

out = make([]string, 0, rv.Len())
for _, k := range rv.MapKeys() {
    out = append(out, k.String())
}
sort.Strings(out)

reflect and sort are already imported in koanf.go for other methods.

Verification

TestMapKeysNativeMap in tests/koanf_test.go loads a map[string]int64 and asserts MapKeys returns its keys.

Reverting only the MapKeys body in koanf.go (test left in place) fails it:

--- FAIL: TestMapKeysNativeMap
    Error:      Not equal:
                expected: []string{"a", "b"}
                actual  : []string{}
    Messages:   map keys mismatch for a natively-typed map

go build ./..., go vet ./... and gofmt -l . are clean in both the root module and tests/ (the only gofmt -l hit is the pre-existing examples/read-huml/main.go, untouched by this change).

go test ./... -count=1 and go test ./... -count=1 -race in tests/: 60 tests, all passing, including the new one.

Flags for the account owner

  • No CLA/DCO step found in CONTRIBUTING.md or .github/.
  • Behaviour change: code that relied on MapKeys returning []string{} for a natively-typed map (rather than actually reading its keys) would see a different result now. I think the old behaviour was simply wrong given the doc comment, but flagging it since #444 called out the same kind of change for its three getters.