Merge pull request #131029 from liggitt/to-unstructured-stdlib

Make ToUnstructured match stdlib omitempty and anonymous behavior
This commit is contained in:
Kubernetes Prow Robot
2025-05-02 08:07:55 -07:00
committed by GitHub
3 changed files with 189 additions and 6 deletions

View File

@@ -376,17 +376,20 @@ func fieldInfoFromField(structType reflect.Type, field int) *fieldInfo {
typeField := structType.Field(field)
jsonTag := typeField.Tag.Get("json")
if len(jsonTag) == 0 {
// Make the first character lowercase.
if typeField.Name == "" {
if !typeField.Anonymous {
// match stdlib behavior for naming fields that don't specify a json tag name
info.name = typeField.Name
} else {
info.name = strings.ToLower(typeField.Name[:1]) + typeField.Name[1:]
}
} else {
items := strings.Split(jsonTag, ",")
info.name = items[0]
if len(info.name) == 0 && !typeField.Anonymous {
// match stdlib behavior for naming fields that don't specify a json tag name
info.name = typeField.Name
}
for i := range items {
if items[i] == "omitempty" {
if i > 0 && items[i] == "omitempty" {
info.omitempty = true
break
}

View File

@@ -998,3 +998,179 @@ func TestCustomToUnstructuredTopLevel(t *testing.T) {
})
}
}
type OmitemptyNameField struct {
I int `json:"omitempty"`
}
func TestOmitempty(t *testing.T) {
expected := `{"omitempty":0}`
o := &OmitemptyNameField{}
jsonData, err := json.Marshal(o)
if err != nil {
t.Fatal(err)
}
if e, a := expected, string(jsonData); e != a {
t.Fatalf("expected\n%s\ngot\n%s", e, a)
}
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&o)
if err != nil {
t.Fatal(err)
}
jsonUnstrData, err := json.Marshal(unstr)
if err != nil {
t.Fatal(err)
}
if e, a := expected, string(jsonUnstrData); e != a {
t.Fatalf("expected\n%s\ngot\n%s", e, a)
}
}
type InlineTestPrimitive struct {
NoNameTagPrimitive int64 `json:""`
NoNameTagInlinePrimitive int64 `json:",inline"`
NoNameTagOmitemptyPrimitive int64 `json:",omitempty"`
}
type InlineTestAnonymous struct {
NoTag
NoNameTag `json:""`
NameTag `json:"nameTagEmbedded"`
NoNameTagInline `json:",inline"`
NoNameTagOmitempty `json:",omitempty"`
}
type InlineTestNamed struct {
NoTag NoTag
NoNameTag NoNameTag `json:""`
NameTag NameTag `json:"nameTagEmbedded"`
NoNameTagInline NoNameTagInline `json:",inline"`
NoNameTagOmitempty NoNameTagOmitempty `json:",omitempty"`
}
type NoTag struct {
Data0 int `json:"data0"`
}
type NameTag struct {
Data1 int `json:"data1"`
}
type NoNameTag struct {
Data2 int `json:"data2"`
}
type NoNameTagInline struct {
Data3 int `json:"data3"`
}
type NoNameTagOmitempty struct {
Data4 int `json:"data4"`
}
func TestInline(t *testing.T) {
testcases := []struct {
name string
obj any
expect map[string]any
}{
{
name: "primitive-zero",
obj: &InlineTestPrimitive{},
expect: map[string]any{
"NoNameTagPrimitive": int64(0),
"NoNameTagInlinePrimitive": int64(0),
},
},
{
name: "primitive-set",
obj: &InlineTestPrimitive{
NoNameTagPrimitive: 1,
NoNameTagInlinePrimitive: 2,
NoNameTagOmitemptyPrimitive: 3,
},
expect: map[string]any{
"NoNameTagPrimitive": int64(1),
"NoNameTagInlinePrimitive": int64(2),
"NoNameTagOmitemptyPrimitive": int64(3),
},
},
{
name: "anonymous-zero",
obj: &InlineTestAnonymous{},
expect: map[string]any{
"data0": int64(0),
"data2": int64(0),
"data3": int64(0),
"data4": int64(0),
"nameTagEmbedded": map[string]any{"data1": int64(0)},
},
},
{
name: "anonymous-set",
obj: &InlineTestAnonymous{},
expect: map[string]any{
"data0": int64(0),
"data2": int64(0),
"data3": int64(0),
"data4": int64(0),
"nameTagEmbedded": map[string]any{"data1": int64(0)},
},
},
{
name: "named-zero",
obj: &InlineTestNamed{},
expect: map[string]any{
"NoTag": map[string]any{"data0": int64(0)},
"nameTagEmbedded": map[string]any{"data1": int64(0)},
"NoNameTag": map[string]any{"data2": int64(0)},
"NoNameTagInline": map[string]any{"data3": int64(0)},
"NoNameTagOmitempty": map[string]any{"data4": int64(0)},
},
},
{
name: "named-set",
obj: &InlineTestNamed{
NoTag: NoTag{Data0: 10},
NameTag: NameTag{Data1: 11},
NoNameTag: NoNameTag{Data2: 12},
NoNameTagInline: NoNameTagInline{Data3: 13},
NoNameTagOmitempty: NoNameTagOmitempty{Data4: 14},
},
expect: map[string]any{
"NoTag": map[string]any{"data0": int64(10)},
"nameTagEmbedded": map[string]any{"data1": int64(11)},
"NoNameTag": map[string]any{"data2": int64(12)},
"NoNameTagInline": map[string]any{"data3": int64(13)},
"NoNameTagOmitempty": map[string]any{"data4": int64(14)},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
defer func() {
// handle panics
if err := recover(); err != nil {
t.Fatal(err)
}
}()
// Check the expectation against stdlib
jsonData, err := json.Marshal(tc.obj)
if err != nil {
t.Fatal(err)
}
jsonUnstr := map[string]any{}
if err := json.Unmarshal(jsonData, &jsonUnstr); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.expect, jsonUnstr) {
t.Fatal(cmp.Diff(tc.expect, jsonUnstr))
}
// Check the expectation against DefaultUnstructuredConverter.ToUnstructured
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.obj)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.expect, unstr) {
t.Fatal(cmp.Diff(tc.expect, unstr))
}
})
}
}

View File

@@ -32,6 +32,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -338,7 +339,10 @@ func TestGoldenResponse(t *testing.T) {
var got []interface{}
for e := range w.ResultChan() {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&e)
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&metav1.WatchEvent{
Type: string(e.Type),
Object: runtime.RawExtension{Object: e.Object},
})
if err != nil {
t.Fatalf("failed to convert watch event to unstructured content: %v", err)
}