diff --git a/mapstructure.go b/mapstructure.go index 9087fd96..b96b8c3d 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -1100,7 +1100,7 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle } for _, k := range dataVal.MapKeys() { - fieldName := name + "[" + k.String() + "]" + fieldName := name + "[" + mapKeyName(k) + "]" // First decode the key into the proper type currentKey := reflect.Indirect(reflect.New(valKeyType)) @@ -1126,6 +1126,26 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle return errors.Join(errs...) } +// mapKeyName renders a map key for error messages and metadata. reflect.Value's +// String method returns the key itself only for a string Kind and a placeholder +// such as "" for anything else, so keys of an +// any-keyed map - what YAML decoders produce - have to be unwrapped first. +func mapKeyName(k reflect.Value) string { + if k.Kind() == reflect.Interface && !k.IsNil() { + k = k.Elem() + } + + if k.Kind() == reflect.String { + return k.String() + } + + if !k.IsValid() || !k.CanInterface() { + return k.String() + } + + return fmt.Sprintf("%v", k.Interface()) +} + func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { typ := dataVal.Type() for i := 0; i < typ.NumField(); i++ { diff --git a/mapstructure_test.go b/mapstructure_test.go index baf40dfe..d8488c91 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -2795,6 +2795,59 @@ func TestMetadata(t *testing.T) { } } +func TestMetadata_MapKeys(t *testing.T) { + t.Parallel() + + type testResult struct { + Vmap map[string]string + } + + // YAML decoders hand mapstructure an any-keyed map: the keys still have to + // be reported by their value, not by reflect.Value's placeholder string. + input := map[string]any{ + "vmap": map[any]any{"foo": "bar", "baz": "qux"}, + } + + var md Metadata + var result testResult + decoder, err := NewDecoder(&DecoderConfig{Metadata: &md, Result: &result}) + if err != nil { + t.Fatalf("err: %s", err) + } + + if err := decoder.Decode(input); err != nil { + t.Fatalf("err: %s", err) + } + + keys := map[string]struct{}{} + for _, k := range md.Keys { + keys[k] = struct{}{} + } + + for _, expected := range []string{"Vmap", "Vmap[foo]", "Vmap[baz]"} { + if _, ok := keys[expected]; !ok { + t.Fatalf("missing key %q: %#v", expected, md.Keys) + } + } +} + +func TestDecodeMapKeyName(t *testing.T) { + t.Parallel() + + var result map[string]string + + // The int key cannot be decoded into a string key, and the error has to + // name the offending key rather than "". + err := Decode(map[any]any{7: "foo"}, &result) + if err == nil { + t.Fatal("expected an error") + } + + if !strings.Contains(err.Error(), "'[7]'") { + t.Fatalf("bad error: %s", err) + } +} + func TestMetadata_Embedded(t *testing.T) { t.Parallel()