mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-12 21:58:58 -04:00
fix(search): nest json-tagged embedded structs instead of flattening them
resolveField marked every anonymous field embedded, so walkFields (mapping, field index, validate) and fillStruct (deserializer) flattened a json-tagged embedded struct, while conversions.To/encoding/json on the write path nests it under the tag, mapping and deserializing it at the wrong path. An anonymous field is now embedded only without a json tag name, matching encoding/json; fillStruct also recurses into a value nested struct. No current type has a tagged embedded struct, so runtime behavior is unchanged; this hardens the reflection walker.
This commit is contained in:
1 parent
dad99e85b2
commit
69c517a8b2
4 files changed
+56
-3
No files matched your search
@@ -87,6 +87,14 @@ func fillStruct[V any](v reflect.Value, fields map[string]V, prefix string, setL
|
||||
}
|
||||
}
|
||||
|
||||
// Value nested struct (e.g. a tagged embedded struct): recurse under key.
|
||||
if fv.Kind() == reflect.Struct && fv.Type() != timeType && fv.Type() != timestampType {
|
||||
if fillStruct(fv, fields, key, setLeaf) {
|
||||
touched = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if raw, ok := fields[key]; ok && setLeaf(fv, raw) == nil {
|
||||
touched = true
|
||||
}
|
||||
|
||||
@@ -32,6 +32,13 @@ type embedded struct {
|
||||
Photo *photo `json:"photo,omitempty"`
|
||||
}
|
||||
|
||||
// taggedEmbedded embeds Leaf with a json tag, so encoding/json nests it under
|
||||
// "leaf" rather than flattening it onto the parent.
|
||||
type taggedEmbedded struct {
|
||||
Leaf `json:"leaf"`
|
||||
Top string `json:"top"`
|
||||
}
|
||||
|
||||
var _ = Describe("Deserialize", func() {
|
||||
It("panics for a non-struct type at a prefix", func() {
|
||||
Expect(func() {
|
||||
@@ -115,4 +122,18 @@ var _ = Describe("Deserialize", func() {
|
||||
Expect(r.Year).ToNot(BeNil())
|
||||
Expect(*r.Year).To(Equal(int32(2024)))
|
||||
})
|
||||
|
||||
It("nests a tagged embedded struct instead of flattening it", func() {
|
||||
// matches encoding/json: a tagged embedded struct is read under its tag.
|
||||
r := Deserialize[taggedEmbedded](map[string]any{
|
||||
"leaf.Name": "n",
|
||||
"top": "t",
|
||||
})
|
||||
Expect(r.Leaf.Name).To(Equal("n"))
|
||||
Expect(r.Top).To(Equal("t"))
|
||||
|
||||
// the flattened top-level key must NOT populate the nested field.
|
||||
flat := Deserialize[taggedEmbedded](map[string]any{"Name": "flat"})
|
||||
Expect(flat.Leaf.Name).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
@@ -77,3 +77,22 @@ func TestFieldNameIndex_CoversAllTopLevelFields(t *testing.T) {
|
||||
require.Equalf(t, want, resolve(idx, in), "derived should cover %q", in)
|
||||
}
|
||||
}
|
||||
|
||||
// NestInner is embedded with a json tag below, so it must nest, not flatten.
|
||||
type NestInner struct {
|
||||
A string `json:"A"`
|
||||
}
|
||||
|
||||
type taggedOuter struct {
|
||||
NestInner `json:"inner"`
|
||||
Top string `json:"top"`
|
||||
}
|
||||
|
||||
// A json-tagged embedded struct nests under its tag in the derived index too
|
||||
// (walkFields must match encoding/json), so its fields are "inner.A", not "A".
|
||||
func TestFieldNameIndex_TaggedEmbeddedNests(t *testing.T) {
|
||||
idx := mapping.FieldNameIndex(reflect.TypeFor[taggedOuter](), nil)
|
||||
require.Equal(t, "inner.A", resolve(idx, "inner.a")) // nested under the tag
|
||||
require.Equal(t, "top", resolve(idx, "top"))
|
||||
require.Equal(t, "a", resolve(idx, "a")) // not flattened: bare "a" is not a key
|
||||
}
|
||||
@@ -58,6 +58,7 @@ func resolveField(sf reflect.StructField) fieldInfo {
|
||||
return fieldInfo{Skip: true}
|
||||
}
|
||||
name := sf.Name
|
||||
named := false
|
||||
tag := sf.Tag.Get("json")
|
||||
if tag != "" {
|
||||
first, _, _ := strings.Cut(tag, ",")
|
||||
@@ -66,12 +67,16 @@ func resolveField(sf reflect.StructField) fieldInfo {
|
||||
}
|
||||
if first != "" {
|
||||
name = first
|
||||
named = true
|
||||
}
|
||||
}
|
||||
return fieldInfo{
|
||||
Name: name,
|
||||
GoField: sf,
|
||||
Embedded: sf.Anonymous,
|
||||
Name: name,
|
||||
GoField: sf,
|
||||
// An anonymous field is embedded (flattened onto the parent) only when it
|
||||
// has no json tag name, matching encoding/json: a tag name nests it as a
|
||||
// regular field instead.
|
||||
Embedded: sf.Anonymous && !named,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user