diff --git a/libs/structs/structdiff/diff.go b/libs/structs/structdiff/diff.go index cd37fff886b..244321f3a66 100644 --- a/libs/structs/structdiff/diff.go +++ b/libs/structs/structdiff/diff.go @@ -356,8 +356,14 @@ func getForceSendFields(v reflect.Value) []string { if !v.IsValid() || v.Kind() != reflect.Struct { return nil } - fsField := v.FieldByName("ForceSendFields") - if !fsField.IsValid() || fsField.Kind() != reflect.Slice { + // Only the struct's own ForceSendFields: one promoted from an embedded struct names that + // struct's fields, since the SDK marshaller rejects names its struct does not have. + sf, ok := v.Type().FieldByName("ForceSendFields") + if !ok || len(sf.Index) != 1 { + return nil + } + fsField := v.Field(sf.Index[0]) + if fsField.Kind() != reflect.Slice { return nil } result, ok := reflect.TypeAssert[[]string](fsField) diff --git a/libs/structs/structdiff/diff_test.go b/libs/structs/structdiff/diff_test.go index fa875bccbbb..187b08f80ba 100644 --- a/libs/structs/structdiff/diff_test.go +++ b/libs/structs/structdiff/diff_test.go @@ -1125,3 +1125,31 @@ func TestGetStructDiffPathsSharingPrefix(t *testing.T) { assert.Equal(t, Change{Path: changes[2].Path, Old: nil, New: "2"}, changes[2]) assert.Equal(t, Change{Path: changes[3].Path, Old: "1", New: nil}, changes[3]) } + +type PromotedInner struct { + A string `json:"a,omitempty"` + ForceSendFields []string `json:"-"` +} + +type promotedOuter struct { + PromotedInner + Own string `json:"own,omitempty"` +} + +// The embedded struct's ForceSendFields does not force the outer struct's own fields; +// the SDK marshaller would reject a name its struct does not have. +func TestGetStructDiffEmbeddedForceSendFieldsDoNotApplyToOuterFields(t *testing.T) { + forced := promotedOuter{ForceSendFields: []string{"A", "Own"}} + plain := promotedOuter{} + + changes, err := GetStructDiff(forced, plain, nil) + require.NoError(t, err) + assert.Equal(t, []ResolvedChange{{Field: "a", Old: "", New: nil}}, resolveChanges(changes)) + assert.False(t, IsEqual(forced, plain)) + + forced.ForceSendFields = []string{"Own"} + changes, err = GetStructDiff(forced, plain, nil) + require.NoError(t, err) + assert.Empty(t, changes) + assert.True(t, IsEqual(forced, plain)) +} diff --git a/libs/structs/structwalk/walk.go b/libs/structs/structwalk/walk.go index 0a1186a4e81..1bbb425d1a8 100644 --- a/libs/structs/structwalk/walk.go +++ b/libs/structs/structwalk/walk.go @@ -155,8 +155,14 @@ func getForceSendFields(v reflect.Value) []string { if !v.IsValid() || v.Kind() != reflect.Struct { return nil } - fsField := v.FieldByName("ForceSendFields") - if !fsField.IsValid() || fsField.Kind() != reflect.Slice { + // Only the struct's own ForceSendFields: one promoted from an embedded struct names that + // struct's fields, since the SDK marshaller rejects names its struct does not have. + sf, ok := v.Type().FieldByName("ForceSendFields") + if !ok || len(sf.Index) != 1 { + return nil + } + fsField := v.Field(sf.Index[0]) + if fsField.Kind() != reflect.Slice { return nil } result, ok := reflect.TypeAssert[[]string](fsField) diff --git a/libs/structs/structwalk/walk_test.go b/libs/structs/structwalk/walk_test.go index e8a3bf7c09c..72db4cd866c 100644 --- a/libs/structs/structwalk/walk_test.go +++ b/libs/structs/structwalk/walk_test.go @@ -320,3 +320,20 @@ func walkPaths(t *testing.T, value any) []string { slices.Sort(paths) return paths } + +type PromotedInner struct { + A string `json:"a,omitempty"` + ForceSendFields []string `json:"-"` +} + +type promotedOuter struct { + PromotedInner + Own string `json:"own,omitempty"` +} + +// The embedded struct's ForceSendFields does not force the outer struct's own fields; +// the SDK marshaller would reject a name its struct does not have. +func TestEmbeddedForceSendFieldsDoNotApplyToOuterFields(t *testing.T) { + v := promotedOuter{ForceSendFields: []string{"A", "Own"}} + assert.Equal(t, map[string]any{"a": ""}, flatten(t, v)) +}