Skip to content

Commit 6b88536

Browse files
Fix embedded struct handling in structwalk (#3765)
## Summary This PR fixes the handling of embedded (anonymous) structs in the `structwalk` package. Previously, embedded struct fields were incorrectly nested under the embedded struct's name. Now they are properly flattened to the same level as the parent struct's fields, matching Go's JSON marshaling behavior. ## Problem Before this fix, when walking a struct with embedded fields like: ```go type Embedded struct { Field string `json:"field"` } type Parent struct { Embedded ParentField string `json:"parent_field"` } ``` The walk would produce paths like: - `Embedded.field` ❌ (incorrect) - `parent_field` ✅ But Go's JSON marshaling flattens embedded fields, so the JSON structure is: ```json { "field": "...", "parent_field": "..." } ``` ## Solution Added a check for `sf.Anonymous` in `walkStruct()` to detect embedded structs and walk them directly without adding a key to the path: ```go // Directly walk into embedded structs without adding the key to the path. if sf.Anonymous { walkValue(path, s.Field(i), &sf, visit) continue } ``` Now both paths are at the same level: - `field` ✅ - `parent_field` ✅ ## Test Coverage New unit tests --------- Co-authored-by: Claude <[email protected]>
1 parent 3d73d8b commit 6b88536

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

libs/structs/structwalk/walk.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ func walkStruct(path *structpath.PathNode, s reflect.Value, visit VisitFunc) {
115115
continue
116116
}
117117

118+
// Directly walk into embedded structs without adding the key to the path.
119+
if sf.Anonymous {
120+
walkValue(path, s.Field(i), &sf, visit)
121+
continue
122+
}
123+
118124
jsonTag := structtag.JSONTag(sf.Tag.Get("json"))
119125
if jsonTag.Name() == "-" {
120126
continue // skip fields without json name

libs/structs/structwalk/walk_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,112 @@ func TestValueBundleTag(t *testing.T) {
127127
assert.Equal(t, []string{"A", "D"}, readonly)
128128
assert.Equal(t, []string{"B", "D"}, internal)
129129
}
130+
131+
func TestEmbeddedStruct(t *testing.T) {
132+
type Embedded struct {
133+
EmbeddedField string `json:"embedded_field"`
134+
EmbeddedInt int `json:"embedded_int"`
135+
}
136+
137+
type Parent struct {
138+
Embedded
139+
ParentField string `json:"parent_field"`
140+
}
141+
142+
parent := Parent{
143+
Embedded: Embedded{
144+
EmbeddedField: "embedded_value",
145+
EmbeddedInt: 42,
146+
},
147+
ParentField: "parent_value",
148+
}
149+
150+
result := flatten(t, parent)
151+
152+
// Embedded struct fields should be at the same level as parent fields
153+
assert.Equal(t, map[string]any{
154+
"embedded_field": "embedded_value",
155+
"embedded_int": 42,
156+
"parent_field": "parent_value",
157+
}, result)
158+
}
159+
160+
func TestNestedEmbeddedStructs(t *testing.T) {
161+
type Level1 struct {
162+
Field1 string `json:"field1"`
163+
}
164+
165+
type Level2 struct {
166+
Level1
167+
Field2 string `json:"field2"`
168+
}
169+
170+
type Level3 struct {
171+
Level2
172+
Field3 string `json:"field3"`
173+
}
174+
175+
obj := Level3{
176+
Level2: Level2{
177+
Level1: Level1{
178+
Field1: "one",
179+
},
180+
Field2: "two",
181+
},
182+
Field3: "three",
183+
}
184+
185+
assert.Equal(t, map[string]any{
186+
"field1": "one",
187+
"field2": "two",
188+
"field3": "three",
189+
}, flatten(t, obj))
190+
}
191+
192+
func TestEmbeddedStructWithPointer(t *testing.T) {
193+
type Embedded struct {
194+
EmbeddedField string `json:"embedded_field"`
195+
}
196+
197+
type Parent struct {
198+
*Embedded
199+
ParentField string `json:"parent_field"`
200+
}
201+
202+
parent := Parent{
203+
Embedded: &Embedded{
204+
EmbeddedField: "embedded_value",
205+
},
206+
ParentField: "parent_value",
207+
}
208+
209+
assert.Equal(t, map[string]any{
210+
"embedded_field": "embedded_value",
211+
"parent_field": "parent_value",
212+
}, flatten(t, parent))
213+
}
214+
215+
func TestEmbeddedStructWithJSONTagDash(t *testing.T) {
216+
type Embedded struct {
217+
SkipField string `json:"-"`
218+
IncludeField string `json:"included"`
219+
}
220+
221+
type Parent struct {
222+
Embedded
223+
ParentField string `json:"parent_field"`
224+
}
225+
226+
parent := Parent{
227+
Embedded: Embedded{
228+
SkipField: "should_not_appear",
229+
IncludeField: "should_appear",
230+
},
231+
ParentField: "parent",
232+
}
233+
234+
assert.Equal(t, map[string]any{
235+
"included": "should_appear",
236+
"parent_field": "parent",
237+
}, flatten(t, parent))
238+
}

0 commit comments

Comments
 (0)