From ead1d6671f14b908429802a6d3177c21d9c80275 Mon Sep 17 00:00:00 2001 From: zhanglangning Date: Mon, 10 Aug 2026 21:34:20 +0800 Subject: [PATCH 1/2] fix: preserve non-nil omitempty struct pointers Change-Id: I9f2b3d140fd5e0bd3a757758a90271ddf2540068 --- encode_test.go | 136 +++++++++++++++++++++++++ internal/cmd/generator/vm.go.tmpl | 29 +++++- internal/encoder/vm/vm.go | 29 +++++- internal/encoder/vm_color/vm.go | 29 +++++- internal/encoder/vm_color_indent/vm.go | 29 +++++- internal/encoder/vm_indent/vm.go | 29 +++++- 6 files changed, 266 insertions(+), 15 deletions(-) diff --git a/encode_test.go b/encode_test.go index e71a9992..f67ec0cf 100644 --- a/encode_test.go +++ b/encode_test.go @@ -2729,3 +2729,139 @@ func TestIssue459(t *testing.T) { assertErr(t, err) assertEq(t, "unexpected result", "{}", string(b)) } + +func TestIssue503(t *testing.T) { + type Child struct { + First int `json:"first"` + Later string `json:"later"` + } + type DirectRoot struct { + Child *Child `json:"child,omitempty"` + } + type BoolChild struct { + First bool `json:"first"` + } + type BoolRoot struct { + Child *BoolChild `json:"child,omitempty"` + } + type StringChild struct { + First string `json:"first"` + } + type StringRoot struct { + Child *StringChild `json:"child,omitempty"` + } + type IndirectRoot struct { + Child *Child `json:"child,omitempty"` + Tail string `json:"tail"` + } + type PrefixedRoot struct { + Prefix string `json:"prefix"` + Child *Child `json:"child,omitempty"` + } + type DoubleRoot struct { + Child **Child `json:"child,omitempty"` + } + type Nested struct { + Child *Child `json:"child,omitempty"` + } + type NestedRoot struct { + Nested *Nested `json:"nested,omitempty"` + } + type NestedWithTail struct { + Child *Child `json:"child,omitempty"` + Tail string `json:"tail"` + } + type NestedWithTailRoot struct { + Nested *NestedWithTail `json:"nested,omitempty"` + } + + var nilChild *Child + child := &Child{} + tests := []struct { + name string + value interface{} + }{ + { + name: "direct pointer with zero first and non-zero later field", + value: DirectRoot{Child: &Child{Later: "kept"}}, + }, + { + name: "direct pointer with all-zero fields", + value: DirectRoot{Child: &Child{}}, + }, + { + name: "direct pointer with false first field", + value: BoolRoot{Child: &BoolChild{}}, + }, + { + name: "direct pointer with empty string first field", + value: StringRoot{Child: &StringChild{}}, + }, + { + name: "direct nil pointer", + value: DirectRoot{}, + }, + { + name: "indirect root with pointer first", + value: IndirectRoot{Child: &Child{}, Tail: "tail"}, + }, + { + name: "indirect root with pointer after prefix", + value: PrefixedRoot{Prefix: "prefix", Child: &Child{}}, + }, + { + name: "double pointer to zero-value child", + value: DoubleRoot{Child: &child}, + }, + { + name: "double pointer with nil inner pointer", + value: DoubleRoot{Child: &nilChild}, + }, + { + name: "double nil pointer", + value: DoubleRoot{}, + }, + { + name: "nested non-nil pointers", + value: NestedRoot{Nested: &Nested{Child: &Child{}}}, + }, + { + name: "nested nil child pointer", + value: NestedRoot{Nested: &Nested{}}, + }, + { + name: "nested nil child pointer with non-zero later field", + value: NestedWithTailRoot{Nested: &NestedWithTail{Tail: "kept"}}, + }, + { + name: "nested child pointer with non-zero later field", + value: NestedWithTailRoot{Nested: &NestedWithTail{Child: &Child{}, Tail: "kept"}}, + }, + { + name: "nested nil pointer", + value: NestedRoot{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + expected, err := stdjson.Marshal(test.value) + assertErr(t, err) + got, err := json.Marshal(test.value) + assertErr(t, err) + assertEq(t, "unexpected result", string(expected), string(got)) + got, err = json.MarshalWithOption(test.value, json.Colorize(&json.ColorScheme{})) + assertErr(t, err) + assertEq(t, "unexpected color result", string(expected), string(got)) + + expected, err = stdjson.MarshalIndent(test.value, "", " ") + assertErr(t, err) + got, err = json.MarshalIndent(test.value, "", " ") + assertErr(t, err) + assertEq(t, "unexpected indented result", string(expected), string(got)) + got, err = json.MarshalIndentWithOption(test.value, "", " ", json.Colorize(&json.ColorScheme{})) + assertErr(t, err) + assertEq(t, "unexpected indented color result", string(expected), string(got)) + }) + } +} diff --git a/internal/cmd/generator/vm.go.tmpl b/internal/cmd/generator/vm.go.tmpl index 645d20f9..6aa74dc6 100644 --- a/internal/cmd/generator/vm.go.tmpl +++ b/internal/cmd/generator/vm.go.tmpl @@ -567,8 +567,28 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b code = code.End.Next break } - store(ctxptr, code.Idx, ptrToNPtr(p, code.PtrNum)) - fallthrough + if (code.Flags & encoder.IndirectFlags) != 0 { + p = ptrToNPtr(p, code.PtrNum) + if p == 0 { + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendNullComma(ctx, b) + } + code = code.End.Next + break + } + } + store(ctxptr, code.Idx, p) + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendStructHead(ctx, b) + } + p += uintptr(code.Offset) + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && ptrToPtr(p) == 0 { + code = code.NextField + } else { + b = appendStructKey(ctx, code, b) + code = code.Next + store(ctxptr, code.Idx, p) + } case encoder.OpStructHeadOmitEmpty: p := load(ctxptr, code.Idx) if p == 0 && ((code.Flags&encoder.IndirectFlags) != 0 || code.Next.Op == encoder.OpStructEnd) { @@ -582,9 +602,12 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b b = appendStructHead(ctx, b) } p += uintptr(code.Offset) - if p == 0 || (ptrToPtr(p) == 0 && (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0) { + if p == 0 || ((code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) != 0 && ptrToPtr(p) == 0) { code = code.NextField } else { + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) == 0 && code.PtrNum > 1 { + p = ptrToNPtr(p, code.PtrNum-1) + } b = appendStructKey(ctx, code, b) code = code.Next store(ctxptr, code.Idx, p) diff --git a/internal/encoder/vm/vm.go b/internal/encoder/vm/vm.go index 645d20f9..6aa74dc6 100644 --- a/internal/encoder/vm/vm.go +++ b/internal/encoder/vm/vm.go @@ -567,8 +567,28 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b code = code.End.Next break } - store(ctxptr, code.Idx, ptrToNPtr(p, code.PtrNum)) - fallthrough + if (code.Flags & encoder.IndirectFlags) != 0 { + p = ptrToNPtr(p, code.PtrNum) + if p == 0 { + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendNullComma(ctx, b) + } + code = code.End.Next + break + } + } + store(ctxptr, code.Idx, p) + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendStructHead(ctx, b) + } + p += uintptr(code.Offset) + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && ptrToPtr(p) == 0 { + code = code.NextField + } else { + b = appendStructKey(ctx, code, b) + code = code.Next + store(ctxptr, code.Idx, p) + } case encoder.OpStructHeadOmitEmpty: p := load(ctxptr, code.Idx) if p == 0 && ((code.Flags&encoder.IndirectFlags) != 0 || code.Next.Op == encoder.OpStructEnd) { @@ -582,9 +602,12 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b b = appendStructHead(ctx, b) } p += uintptr(code.Offset) - if p == 0 || (ptrToPtr(p) == 0 && (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0) { + if p == 0 || ((code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) != 0 && ptrToPtr(p) == 0) { code = code.NextField } else { + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) == 0 && code.PtrNum > 1 { + p = ptrToNPtr(p, code.PtrNum-1) + } b = appendStructKey(ctx, code, b) code = code.Next store(ctxptr, code.Idx, p) diff --git a/internal/encoder/vm_color/vm.go b/internal/encoder/vm_color/vm.go index a63e83e5..e1d08437 100644 --- a/internal/encoder/vm_color/vm.go +++ b/internal/encoder/vm_color/vm.go @@ -567,8 +567,28 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b code = code.End.Next break } - store(ctxptr, code.Idx, ptrToNPtr(p, code.PtrNum)) - fallthrough + if (code.Flags & encoder.IndirectFlags) != 0 { + p = ptrToNPtr(p, code.PtrNum) + if p == 0 { + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendNullComma(ctx, b) + } + code = code.End.Next + break + } + } + store(ctxptr, code.Idx, p) + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendStructHead(ctx, b) + } + p += uintptr(code.Offset) + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && ptrToPtr(p) == 0 { + code = code.NextField + } else { + b = appendStructKey(ctx, code, b) + code = code.Next + store(ctxptr, code.Idx, p) + } case encoder.OpStructHeadOmitEmpty: p := load(ctxptr, code.Idx) if p == 0 && ((code.Flags&encoder.IndirectFlags) != 0 || code.Next.Op == encoder.OpStructEnd) { @@ -582,9 +602,12 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b b = appendStructHead(ctx, b) } p += uintptr(code.Offset) - if p == 0 || (ptrToPtr(p) == 0 && (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0) { + if p == 0 || ((code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) != 0 && ptrToPtr(p) == 0) { code = code.NextField } else { + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) == 0 && code.PtrNum > 1 { + p = ptrToNPtr(p, code.PtrNum-1) + } b = appendStructKey(ctx, code, b) code = code.Next store(ctxptr, code.Idx, p) diff --git a/internal/encoder/vm_color_indent/vm.go b/internal/encoder/vm_color_indent/vm.go index 3b4e22e5..4e40314f 100644 --- a/internal/encoder/vm_color_indent/vm.go +++ b/internal/encoder/vm_color_indent/vm.go @@ -567,8 +567,28 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b code = code.End.Next break } - store(ctxptr, code.Idx, ptrToNPtr(p, code.PtrNum)) - fallthrough + if (code.Flags & encoder.IndirectFlags) != 0 { + p = ptrToNPtr(p, code.PtrNum) + if p == 0 { + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendNullComma(ctx, b) + } + code = code.End.Next + break + } + } + store(ctxptr, code.Idx, p) + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendStructHead(ctx, b) + } + p += uintptr(code.Offset) + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && ptrToPtr(p) == 0 { + code = code.NextField + } else { + b = appendStructKey(ctx, code, b) + code = code.Next + store(ctxptr, code.Idx, p) + } case encoder.OpStructHeadOmitEmpty: p := load(ctxptr, code.Idx) if p == 0 && ((code.Flags&encoder.IndirectFlags) != 0 || code.Next.Op == encoder.OpStructEnd) { @@ -582,9 +602,12 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b b = appendStructHead(ctx, b) } p += uintptr(code.Offset) - if p == 0 || (ptrToPtr(p) == 0 && (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0) { + if p == 0 || ((code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) != 0 && ptrToPtr(p) == 0) { code = code.NextField } else { + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) == 0 && code.PtrNum > 1 { + p = ptrToNPtr(p, code.PtrNum-1) + } b = appendStructKey(ctx, code, b) code = code.Next store(ctxptr, code.Idx, p) diff --git a/internal/encoder/vm_indent/vm.go b/internal/encoder/vm_indent/vm.go index 836c5c8a..e3d44898 100644 --- a/internal/encoder/vm_indent/vm.go +++ b/internal/encoder/vm_indent/vm.go @@ -567,8 +567,28 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b code = code.End.Next break } - store(ctxptr, code.Idx, ptrToNPtr(p, code.PtrNum)) - fallthrough + if (code.Flags & encoder.IndirectFlags) != 0 { + p = ptrToNPtr(p, code.PtrNum) + if p == 0 { + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendNullComma(ctx, b) + } + code = code.End.Next + break + } + } + store(ctxptr, code.Idx, p) + if code.Flags&encoder.AnonymousHeadFlags == 0 { + b = appendStructHead(ctx, b) + } + p += uintptr(code.Offset) + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && ptrToPtr(p) == 0 { + code = code.NextField + } else { + b = appendStructKey(ctx, code, b) + code = code.Next + store(ctxptr, code.Idx, p) + } case encoder.OpStructHeadOmitEmpty: p := load(ctxptr, code.Idx) if p == 0 && ((code.Flags&encoder.IndirectFlags) != 0 || code.Next.Op == encoder.OpStructEnd) { @@ -582,9 +602,12 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b b = appendStructHead(ctx, b) } p += uintptr(code.Offset) - if p == 0 || (ptrToPtr(p) == 0 && (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0) { + if p == 0 || ((code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) != 0 && ptrToPtr(p) == 0) { code = code.NextField } else { + if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) == 0 && code.PtrNum > 1 { + p = ptrToNPtr(p, code.PtrNum-1) + } b = appendStructKey(ctx, code, b) code = code.Next store(ctxptr, code.Idx, p) From 32ffc6c5092eb3a29b8c8608698ef54e31bca9a5 Mon Sep 17 00:00:00 2001 From: zhanglangning Date: Thu, 13 Aug 2026 03:57:00 +0800 Subject: [PATCH 2/2] fix: preserve pointer depth for direct-interface structs --- encode_test.go | 76 ++++++++++++++++++++++++++++++++++++ internal/encoder/compiler.go | 61 ++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/encode_test.go b/encode_test.go index f67ec0cf..4a9f5dbd 100644 --- a/encode_test.go +++ b/encode_test.go @@ -2730,6 +2730,35 @@ func TestIssue459(t *testing.T) { assertEq(t, "unexpected result", "{}", string(b)) } +type issue503IgnoredBackReferenceRoot struct { + Child *issue503IgnoredBackReferenceChild `json:"child,omitempty"` +} + +type issue503IgnoredBackReferenceChild struct { + Root *issue503IgnoredBackReferenceRoot `json:"-"` + First *int `json:"first,omitempty"` + Value *string `json:"value,omitempty"` +} + +type issue503UnexportedBackReferenceRoot struct { + Child *issue503UnexportedBackReferenceChild `json:"child,omitempty"` +} + +type issue503UnexportedBackReferenceChild struct { + root *issue503UnexportedBackReferenceRoot + First *int `json:"first,omitempty"` + Value *string `json:"value,omitempty"` +} + +type issue503RecursiveRoot struct { + Child *issue503RecursiveChild `json:"child,omitempty"` +} + +type issue503RecursiveChild struct { + Root *issue503RecursiveRoot `json:"root,omitempty"` + Value string `json:"value,omitempty"` +} + func TestIssue503(t *testing.T) { type Child struct { First int `json:"first"` @@ -2774,9 +2803,28 @@ func TestIssue503(t *testing.T) { type NestedWithTailRoot struct { Nested *NestedWithTail `json:"nested,omitempty"` } + type PointerFieldChild struct { + First *int `json:"first,omitempty"` + Value *string `json:"value,omitempty"` + } + type PointerFieldRoot struct { + Child *PointerFieldChild `json:"child,omitempty"` + } + type DoublePointerFieldRoot struct { + Child **PointerFieldChild `json:"child,omitempty"` + } + type IgnoredMarkerRoot struct { + Marker struct{} `json:"-"` + Child *PointerFieldChild `json:"child,omitempty"` + } + type SelfRecursiveRoot struct { + Next *SelfRecursiveRoot `json:"next,omitempty"` + } var nilChild *Child child := &Child{} + pointerValue := "present" + pointerFieldChild := &PointerFieldChild{Value: &pointerValue} tests := []struct { name string value interface{} @@ -2841,6 +2889,34 @@ func TestIssue503(t *testing.T) { name: "nested nil pointer", value: NestedRoot{}, }, + { + name: "direct pointer with pointer fields", + value: PointerFieldRoot{Child: pointerFieldChild}, + }, + { + name: "direct double pointer with pointer fields", + value: DoublePointerFieldRoot{Child: &pointerFieldChild}, + }, + { + name: "direct pointer after ignored zero-size marker", + value: IgnoredMarkerRoot{Child: pointerFieldChild}, + }, + { + name: "direct pointer with ignored tagged back reference", + value: issue503IgnoredBackReferenceRoot{Child: &issue503IgnoredBackReferenceChild{Value: &pointerValue}}, + }, + { + name: "direct pointer with unexported back reference", + value: issue503UnexportedBackReferenceRoot{Child: &issue503UnexportedBackReferenceChild{Value: &pointerValue}}, + }, + { + name: "visible recursive back reference", + value: issue503RecursiveRoot{Child: &issue503RecursiveChild{Value: "present"}}, + }, + { + name: "self-recursive root", + value: SelfRecursiveRoot{Next: &SelfRecursiveRoot{}}, + }, } for _, test := range tests { diff --git a/internal/encoder/compiler.go b/internal/encoder/compiler.go index dca68061..ea8fee38 100644 --- a/internal/encoder/compiler.go +++ b/internal/encoder/compiler.go @@ -119,6 +119,7 @@ func getFilteredCodeSetIfNeeded(ctx *RuntimeContext, codeSet *OpcodeSet) (*Opcod type Compiler struct { structTypeToCode map[uintptr]*StructCode + rootType *runtime.Type } func newCompiler() *Compiler { @@ -130,6 +131,7 @@ func newCompiler() *Compiler { func (c *Compiler) compile(typeptr uintptr) (*OpcodeSet, error) { // noescape trick for header.typ ( reflect.*rtype ) typ := *(**runtime.Type)(unsafe.Pointer(&typeptr)) + c.rootType = typ code, err := c.typeToCode(typ) if err != nil { return nil, err @@ -630,7 +632,7 @@ func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error if indirect { // if parent is indirect type, set child indirect property to true structCode.isIndirect = true - } else { + } else if !c.normalizeDirectInterfaceStructField(code, field, structCode, isPtr) { // if parent is not indirect type, set child indirect property to false. // but if parent's indirect is false and isPtr is true, then indirect must be true. // Do this only if indirectConversion is enabled at the end of compileStruct. @@ -650,6 +652,63 @@ func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error return code, nil } +// normalizeDirectInterfaceStructField removes the pointer level already consumed by +// the interface representation of a direct-interface root struct. Nested instances +// still use the regular addressed representation, so recursive roots are unchanged. +func (c *Compiler) normalizeDirectInterfaceStructField(parent *StructCode, field *StructFieldCode, child *StructCode, isPtr bool) bool { + if isPtr || parent.typ != c.rootType || runtime.IfaceIndir(parent.typ) { + return false + } + ptr, ok := field.value.(*PtrCode) + if !ok || ptr.ptrNum == 0 || ptr.value != child { + return false + } + if codeReferencesType(child, parent.typ, map[Code]struct{}{}) { + return false + } + + if ptr.ptrNum == 1 { + field.value = child + field.isNextOpPtrType = false + } else { + field.value = &PtrCode{ + typ: ptr.typ.Elem(), + value: ptr.value, + ptrNum: ptr.ptrNum - 1, + } + } + child.isIndirect = true + return true +} + +func codeReferencesType(code Code, target *runtime.Type, seen map[Code]struct{}) bool { + if structCode, ok := code.(*StructCode); ok && structCode.typ == target { + return true + } + if _, exists := seen[code]; exists { + return false + } + seen[code] = struct{}{} + + switch code := code.(type) { + case *PtrCode: + return codeReferencesType(code.value, target, seen) + case *StructCode: + for _, field := range code.fields { + if codeReferencesType(field.value, target, seen) { + return true + } + } + case *SliceCode: + return codeReferencesType(code.value, target, seen) + case *ArrayCode: + return codeReferencesType(code.value, target, seen) + case *MapCode: + return codeReferencesType(code.key, target, seen) || codeReferencesType(code.value, target, seen) + } + return false +} + func toElemType(t *runtime.Type) *runtime.Type { for t.Kind() == reflect.Ptr { t = t.Elem()