From 4b3dff0c86c0016224e3517f3a2b61010d7a684d Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 28 Oct 2026 15:27:01 +0100 Subject: [PATCH] structs: use only a struct's own ForceSendFields in structdiff and structwalk FieldByName also finds a ForceSendFields promoted from an embedded struct. That list can only name the embedded struct's fields (the SDK marshaller rejects other names), so it must not decide whether the outer struct's own fields are forced. structaccess and libs/dyn/convert already look only at the declaring struct. Co-authored-by: Isaac --- libs/structs/structdiff/diff.go | 10 ++++++++-- libs/structs/structdiff/diff_test.go | 28 ++++++++++++++++++++++++++++ libs/structs/structwalk/walk.go | 10 ++++++++-- libs/structs/structwalk/walk_test.go | 17 +++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) 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)) +}