From 39359628d950d7268e96a4eecdfac9dd047b9e2d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 5 Oct 2026 11:43:48 +0200 Subject: [PATCH] structaccess: say parent is nil when setting through a nil parent Set returned 'cannot set key "k" on invalid' for nil map/pointer parents omitted via omitempty. Report the parent path as nil instead. Also avoids a panic when writing into a nil map. Co-authored-by: Isaac --- libs/structs/structaccess/set.go | 13 ++++++++- libs/structs/structaccess/set_test.go | 40 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/libs/structs/structaccess/set.go b/libs/structs/structaccess/set.go index 02752e456ea..20752e63e0d 100644 --- a/libs/structs/structaccess/set.go +++ b/libs/structs/structaccess/set.go @@ -70,11 +70,17 @@ func setValueAtNode(parentVal reflect.Value, node *structpath.PathNode, value an // Dereference parent if it's a pointer for parentVal.Kind() == reflect.Pointer { if parentVal.IsNil() { - return errors.New("parent is nil pointer") + return nilParentError(node) } parentVal = parentVal.Elem() } + // getValue returns an invalid value for a nil parent omitted via json omitempty. + // Writing into a nil map would panic. + if !parentVal.IsValid() || ((parentVal.Kind() == reflect.Map || parentVal.Kind() == reflect.Slice) && parentVal.IsNil()) { + return nilParentError(node) + } + valueVal := reflect.ValueOf(value) if idx, isIndex := node.Index(); isIndex { @@ -101,6 +107,11 @@ func setValueAtNode(parentVal reflect.Value, node *structpath.PathNode, value an return errors.New("unsupported path node type") } +// nilParentError reports that the parent of node is nil. Missing parents are not allocated. +func nilParentError(node *structpath.PathNode) error { + return fmt.Errorf("cannot set %s: parent %s is nil", node.String(), node.Parent().String()) +} + // setArrayElement sets an element in an array or slice func setArrayElement(parentVal reflect.Value, index int, valueVal reflect.Value) error { kind := parentVal.Kind() diff --git a/libs/structs/structaccess/set_test.go b/libs/structs/structaccess/set_test.go index 0d55b9881f8..7921e226a52 100644 --- a/libs/structs/structaccess/set_test.go +++ b/libs/structs/structaccess/set_test.go @@ -910,3 +910,43 @@ func TestGetSet_DashIsAFieldNameWhenTheTagHasOptions(t *testing.T) { assert.Equal(t, "set", target.Named, "Set must write to Named") assert.Equal(t, "s", target.Skipped, "json:\"-\" field must stay out of reach") } + +func TestSet_NilParent(t *testing.T) { + type Leaf struct { + N int `json:"n,omitempty"` + } + type Mid struct { + C *Leaf `json:"c,omitempty"` + } + type T struct { + M map[string]string `json:"m,omitempty"` + P *Leaf `json:"p,omitempty"` + A *Mid `json:"a,omitempty"` + S []string `json:"s,omitempty"` + NM map[string]string `json:"nm"` + NP *Leaf `json:"np"` + NS []string `json:"ns"` + } + + tests := []struct { + path string + errorMsg string + }{ + {"m.k", "cannot set m.k: parent m is nil"}, + {"p.n", "cannot set p.n: parent p is nil"}, + {"a.c.n", "cannot set a.c.n: parent a.c is nil"}, + {"s[0]", "cannot set s[0]: parent s is nil"}, + {"nm.k", "cannot set nm.k: parent nm is nil"}, + {"np.n", "cannot set np.n: parent np is nil"}, + {"ns[0]", "cannot set ns[0]: parent ns is nil"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + target := &T{A: &Mid{}} + err := structaccess.SetByString(target, tt.path, "5") + assert.EqualError(t, err, tt.errorMsg) + assert.Equal(t, &T{A: &Mid{}}, target) + }) + } +}