From ded91ac9bf69d7a657f08f428bb9df55a4e018f9 Mon Sep 17 00:00:00 2001 From: Dmitrii Aleksandrov Date: Fri, 16 Oct 2026 08:25:49 +0300 Subject: [PATCH] fix: GetTagPath: embedded structs without the name in the tag are transparent, like in encoding/json Signed-off-by: Dmitrii Aleksandrov --- Readme.md | 2 ++ field.go | 23 +++++++++++++-------- field_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 2 ++ 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/Readme.md b/Readme.md index 7a08076..fbe7270 100644 --- a/Readme.md +++ b/Readme.md @@ -86,6 +86,8 @@ type Field interface { // - tag: string, representing the tag name. // - ignoreParentTagMissing: bool, representing whether to ignore the missing parent tags or not. // It returns the tag value path as a string. + // Embedded structs without the name in the tag are transparent: their fields are on the level of the embedding struct, + // like in encoding/json, so they are not missing parents. GetTagPath(tag string, ignoreParentTagMissing bool) string // GetParent returns the parent field of the current field, if not exist return nil. diff --git a/field.go b/field.go index 66186a5..6a947f3 100644 --- a/field.go +++ b/field.go @@ -175,21 +175,26 @@ func (f *field) Get(obj interface{}) interface{} { } } +// tagName returns the name from the tag value, the part before the comma: json:"name,omitempty" -> name. +func (f *field) tagName(tag string) string { + return strings.Split(f.Tag.Get(tag), ",")[0] +} + func (f *field) GetTagPath(tag string, ignoreParentTagMissing bool) string { - tagPath := "" - if val, ok := f.Tag.Lookup(tag); ok { - vals := strings.Split(val, ",") - if len(vals) > 0 && len(vals[0]) > 0 { - tagPath = vals[0] - } - } + tagPath := f.tagName(tag) if tagPath == "" { return tagPath } - if f.parent == nil { + // embedded structs without the name in the tag are transparent, their fields are on the level of the embedding struct, + // like in encoding/json and the most of the encoding libraries; yaml:",inline" has no name too + parent := f.parent + for parent != nil && parent.Anonymous && parent.tagName(tag) == "" { + parent = parent.parent + } + if parent == nil { return tagPath } - parentTag := f.parent.GetTagPath(tag, ignoreParentTagMissing) + parentTag := parent.GetTagPath(tag, ignoreParentTagMissing) if parentTag == "" && !ignoreParentTagMissing { return "" } diff --git a/field_test.go b/field_test.go index d053681..15eec00 100644 --- a/field_test.go +++ b/field_test.go @@ -539,3 +539,60 @@ func TestField_SetNamedTypes(t *testing.T) { }, *conf) assert.Same(t, &duration, conf.DurationPtr) } + +type tagPathCommon struct { + Port int `json:"port" yaml:"port"` +} + +type tagPathDeep struct { + tagPathCommon + Level string `json:"level" yaml:"level"` +} + +type tagPathConfig struct { + tagPathCommon + HTTP struct { + *tagPathDeep + Host string `json:"host" yaml:"host"` + } `json:"http" yaml:"http"` + Named struct { + tagPathCommon `json:"common" yaml:",inline"` + } `json:"named" yaml:"named"` + NoTag struct { + tagPathCommon + } +} + +func TestField_GetTagPath_Embedded(t *testing.T) { + storage, err := Get[tagPathConfig]() + if err != nil { + t.Fatal(err) + } + tests := []struct { + path string + tag string + ignoreParentTag bool + want string + }{ + // fields of the embedded structs are on the level of the embedding struct + {"tagPathCommon.Port", "json", false, "port"}, + {"HTTP.tagPathDeep.Level", "json", false, "http.level"}, + // embedded into embedded, stored by pointer + {"HTTP.tagPathDeep.tagPathCommon.Port", "json", false, "http.port"}, + {"HTTP.tagPathDeep.tagPathCommon.Port", "json", true, "http.port"}, + // the name in the tag of the embedded struct is used + {"Named.tagPathCommon.Port", "json", false, "named.common.port"}, + // the tag without the name, inline for example, is transparent too + {"Named.tagPathCommon.Port", "yaml", false, "named.port"}, + // the named parent without the tag is missing + {"NoTag.tagPathCommon.Port", "json", false, ""}, + {"NoTag.tagPathCommon.Port", "json", true, "port"}, + } + for _, tt := range tests { + t.Run(tt.path+" "+tt.tag, func(t *testing.T) { + if got := storage.MustFind(tt.path).GetTagPath(tt.tag, tt.ignoreParentTag); got != tt.want { + t.Errorf("field.GetTagPath() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/types.go b/types.go index 4a07198..c5c6c55 100644 --- a/types.go +++ b/types.go @@ -76,6 +76,8 @@ type Field interface { // - tag: string, representing the tag name. // - ignoreParentTagMissing: bool, representing whether to ignore the missing parent tags or not. // It returns the tag value path as a string. + // Embedded structs without the name in the tag are transparent: their fields are on the level of the embedding struct, + // like in encoding/json, so they are not missing parents. GetTagPath(tag string, ignoreParentTagMissing bool) string // GetParent returns the parent field of the current field, if not exist return nil.