Skip to content

Commit e8dae37

Browse files
committed
Implement json/v2 style field precedence rules
This commit implements encoding/json/v2 style field precedence rules for struct field resolution, replacing the previous two-phase processing with a single-phase approach that properly handles field conflicts using depth-based and tag-based precedence. Key changes: - Replace fieldsType.anonymousFields with fieldInfo metadata structure - Implement breadth-first traversal for field collection with depth tracking - Add support for embedded pointer types (*EmbeddedStruct) - Apply json/v2 precedence rules: shallow beats deep, tagged beats untagged - Use single-phase processing with FieldByIndex for embedded field access - Initialize nil embedded pointers during field traversal Precedence rules applied: 1. Shallowest embedding depth wins 2. Among same depth, explicitly tagged field wins over untagged 3. Among same depth and tag status, first declared wins Fixes embedded pointer field access that was causing nil pointer dereferences in complex nested structures.
1 parent 2b2d500 commit e8dae37

File tree

2 files changed

+313
-28
lines changed

2 files changed

+313
-28
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package decoder
2+
3+
import (
4+
"encoding/hex"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestFieldPrecedenceRules tests json/v2 style field precedence behavior.
13+
func TestFieldPrecedenceRules(t *testing.T) {
14+
// Test data: {"en": "Foo"}
15+
testData := "e142656e43466f6f"
16+
testBytes, err := hex.DecodeString(testData)
17+
require.NoError(t, err)
18+
19+
t.Run("DirectFieldWinsOverEmbedded", func(t *testing.T) {
20+
type Embedded struct {
21+
En string `maxminddb:"en"`
22+
}
23+
target := &struct {
24+
Embedded
25+
26+
En string `maxminddb:"en"` // Direct field should win
27+
}{}
28+
29+
decoder := New(testBytes)
30+
err := decoder.Decode(0, target)
31+
require.NoError(t, err)
32+
33+
assert.Equal(t, "Foo", target.En, "Direct field should be set")
34+
assert.Empty(t, target.Embedded.En, "Embedded field should not be set due to precedence")
35+
})
36+
37+
t.Run("TaggedFieldWinsOverUntagged", func(t *testing.T) {
38+
type Untagged struct {
39+
En string // Untagged field
40+
}
41+
target := &struct {
42+
Untagged
43+
44+
En string `maxminddb:"en"` // Tagged field should win
45+
}{}
46+
47+
decoder := New(testBytes)
48+
err := decoder.Decode(0, target)
49+
require.NoError(t, err)
50+
51+
assert.Equal(t, "Foo", target.En, "Tagged field should be set")
52+
assert.Empty(t, target.Untagged.En, "Untagged field should not be set")
53+
})
54+
55+
t.Run("ShallowFieldWinsOverDeep", func(t *testing.T) {
56+
type DeepNested struct {
57+
En string `maxminddb:"en"` // Deeper field
58+
}
59+
type MiddleNested struct {
60+
DeepNested
61+
}
62+
target := &struct {
63+
MiddleNested
64+
65+
En string `maxminddb:"en"` // Shallow field should win
66+
}{}
67+
68+
decoder := New(testBytes)
69+
err := decoder.Decode(0, target)
70+
require.NoError(t, err)
71+
72+
assert.Equal(t, "Foo", target.En, "Shallow field should be set")
73+
assert.Empty(t, target.DeepNested.En, "Deep field should not be set due to precedence")
74+
})
75+
}
76+
77+
// TestEmbeddedPointerSupport tests support for embedded pointer types.
78+
func TestEmbeddedPointerSupport(t *testing.T) {
79+
// Test data: {"data": "test"}
80+
testData := "e144646174614474657374"
81+
testBytes, err := hex.DecodeString(testData)
82+
require.NoError(t, err)
83+
84+
type EmbeddedPointer struct {
85+
Data string `maxminddb:"data"`
86+
}
87+
88+
target := &struct {
89+
*EmbeddedPointer
90+
91+
Other string `maxminddb:"other"`
92+
}{}
93+
94+
decoder := New(testBytes)
95+
err = decoder.Decode(0, target)
96+
require.NoError(t, err)
97+
98+
// Test embedded pointer field access - this was causing nil pointer dereference before fix
99+
require.NotNil(t, target.EmbeddedPointer, "Embedded pointer should be initialized")
100+
assert.Equal(t, "test", target.Data)
101+
}
102+
103+
// TestFieldCaching tests the field caching mechanism works with new precedence rules.
104+
func TestFieldCaching(t *testing.T) {
105+
type Embedded struct {
106+
Field1 string `maxminddb:"field1"`
107+
}
108+
109+
type TestStruct struct {
110+
Embedded
111+
112+
Field2 int `maxminddb:"field2"`
113+
Field3 bool `maxminddb:"field3"`
114+
}
115+
116+
// Test that multiple instances use cached fields
117+
s1 := TestStruct{}
118+
s2 := TestStruct{}
119+
120+
fields1 := cachedFields(reflect.ValueOf(s1))
121+
fields2 := cachedFields(reflect.ValueOf(s2))
122+
123+
// Should be the same cached instance
124+
assert.Same(t, fields1, fields2, "Same struct type should use cached fields")
125+
126+
// Verify field mapping includes embedded fields
127+
expectedFieldNames := []string{"field1", "field2", "field3"}
128+
129+
assert.Len(t, fields1.namedFields, 3, "Should have 3 named fields")
130+
for _, name := range expectedFieldNames {
131+
assert.Contains(t, fields1.namedFields, name, "Should contain field: "+name)
132+
assert.NotNil(t, fields1.namedFields[name], "Field info should not be nil: "+name)
133+
}
134+
}

internal/decoder/reflection.go

Lines changed: 179 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -694,15 +694,7 @@ func (d *ReflectionDecoder) decodeStruct(
694694
) (uint, error) {
695695
fields := cachedFields(result)
696696

697-
// This fills in embedded structs
698-
for _, i := range fields.anonymousFields {
699-
_, err := d.unmarshalMap(size, offset, result.Field(i), depth)
700-
if err != nil {
701-
return 0, err
702-
}
703-
}
704-
705-
// This handles named fields
697+
// Single-phase processing: decode only the dominant fields
706698
for range size {
707699
var (
708700
err error
@@ -714,7 +706,7 @@ func (d *ReflectionDecoder) decodeStruct(
714706
}
715707
// The string() does not create a copy due to this compiler
716708
// optimization: https://github.com/golang/go/issues/3512
717-
j, ok := fields.namedFields[string(key)]
709+
fieldInfo, ok := fields.namedFields[string(key)]
718710
if !ok {
719711
offset, err = d.nextValueOffset(offset, 1)
720712
if err != nil {
@@ -723,17 +715,84 @@ func (d *ReflectionDecoder) decodeStruct(
723715
continue
724716
}
725717

726-
offset, err = d.decode(offset, result.Field(j), depth)
718+
// Use FieldByIndex to access fields through their index path
719+
// This handles embedded structs correctly, but we need to initialize
720+
// any nil embedded pointers along the path
721+
fieldValue := result
722+
for i, idx := range fieldInfo.index {
723+
fieldValue = fieldValue.Field(idx)
724+
// If this is an embedded pointer field and it's nil, initialize it
725+
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
726+
// Only initialize if this isn't the final field in the path
727+
if i < len(fieldInfo.index)-1 {
728+
fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
729+
}
730+
}
731+
// If it's a pointer, dereference it to continue traversal
732+
if fieldValue.Kind() == reflect.Ptr && !fieldValue.IsNil() &&
733+
i < len(fieldInfo.index)-1 {
734+
fieldValue = fieldValue.Elem()
735+
}
736+
}
737+
offset, err = d.decode(offset, fieldValue, depth)
727738
if err != nil {
728739
return 0, d.wrapErrorWithMapKey(err, string(key))
729740
}
730741
}
731742
return offset, nil
732743
}
733744

745+
type fieldInfo struct {
746+
name string
747+
index []int
748+
depth int
749+
hasTag bool
750+
}
751+
734752
type fieldsType struct {
735-
namedFields map[string]int
736-
anonymousFields []int
753+
namedFields map[string]*fieldInfo // Map from field name to field info
754+
}
755+
756+
type queueEntry struct {
757+
typ reflect.Type
758+
index []int // Field index path
759+
depth int // Embedding depth
760+
}
761+
762+
// getEmbeddedStructType returns the struct type for embedded fields.
763+
// Returns nil if the field is not an embeddable struct type.
764+
func getEmbeddedStructType(fieldType reflect.Type) reflect.Type {
765+
if fieldType.Kind() == reflect.Struct {
766+
return fieldType
767+
}
768+
if fieldType.Kind() == reflect.Ptr && fieldType.Elem().Kind() == reflect.Struct {
769+
return fieldType.Elem()
770+
}
771+
return nil
772+
}
773+
774+
// handleEmbeddedField processes an embedded struct field and returns true if the field should be skipped.
775+
func handleEmbeddedField(
776+
field reflect.StructField,
777+
hasTag bool,
778+
queue *[]queueEntry,
779+
seen *map[reflect.Type]bool,
780+
fieldIndex []int,
781+
depth int,
782+
) bool {
783+
embeddedType := getEmbeddedStructType(field.Type)
784+
if embeddedType == nil {
785+
return false
786+
}
787+
788+
// For embedded structs (and pointer to structs), add to queue for further traversal
789+
if !(*seen)[embeddedType] {
790+
*queue = append(*queue, queueEntry{embeddedType, fieldIndex, depth + 1})
791+
(*seen)[embeddedType] = true
792+
}
793+
794+
// If embedded struct has no explicit tag, don't add it as a named field
795+
return !hasTag
737796
}
738797

739798
var fieldsMap sync.Map
@@ -744,27 +803,119 @@ func cachedFields(result reflect.Value) *fieldsType {
744803
if fields, ok := fieldsMap.Load(resultType); ok {
745804
return fields.(*fieldsType)
746805
}
747-
numFields := resultType.NumField()
748-
namedFields := make(map[string]int, numFields)
749-
var anonymous []int
750-
for i := range numFields {
751-
field := resultType.Field(i)
752806

753-
fieldName := field.Name
754-
if tag := field.Tag.Get("maxminddb"); tag != "" {
755-
if tag == "-" {
807+
fields := makeStructFields(resultType)
808+
fieldsMap.Store(resultType, fields)
809+
810+
return fields
811+
}
812+
813+
// makeStructFields implements json/v2 style field precedence rules.
814+
func makeStructFields(rootType reflect.Type) *fieldsType {
815+
// Breadth-first traversal to collect all fields with depth information
816+
817+
queue := []queueEntry{{rootType, nil, 0}}
818+
var allFields []fieldInfo
819+
seen := make(map[reflect.Type]bool)
820+
seen[rootType] = true
821+
822+
// Collect all reachable fields using breadth-first search
823+
for len(queue) > 0 {
824+
entry := queue[0]
825+
queue = queue[1:]
826+
827+
for i := range entry.typ.NumField() {
828+
field := entry.typ.Field(i)
829+
830+
// Skip unexported fields (except embedded structs)
831+
if !field.IsExported() && (!field.Anonymous || field.Type.Kind() != reflect.Struct) {
832+
continue
833+
}
834+
835+
// Build field index path
836+
fieldIndex := make([]int, len(entry.index)+1)
837+
copy(fieldIndex, entry.index)
838+
fieldIndex[len(entry.index)] = i
839+
840+
// Parse maxminddb tag
841+
fieldName := field.Name
842+
hasTag := false
843+
if tag := field.Tag.Get("maxminddb"); tag != "" {
844+
if tag == "-" {
845+
continue // Skip ignored fields
846+
}
847+
fieldName = tag
848+
hasTag = true
849+
}
850+
851+
// Handle embedded structs and embedded pointers to structs
852+
if field.Anonymous && handleEmbeddedField(
853+
field, hasTag, &queue, &seen, fieldIndex, entry.depth,
854+
) {
756855
continue
757856
}
758-
fieldName = tag
857+
858+
// Add field to collection
859+
allFields = append(allFields, fieldInfo{
860+
index: fieldIndex,
861+
name: fieldName,
862+
hasTag: hasTag,
863+
depth: entry.depth,
864+
})
759865
}
760-
if field.Anonymous {
761-
anonymous = append(anonymous, i)
866+
}
867+
868+
// Apply precedence rules to resolve field conflicts
869+
namedFields := make(map[string]*fieldInfo)
870+
fieldsByName := make(map[string][]fieldInfo)
871+
872+
// Group fields by name
873+
for _, field := range allFields {
874+
fieldsByName[field.name] = append(fieldsByName[field.name], field)
875+
}
876+
877+
// Apply precedence rules for each field name
878+
for name, fields := range fieldsByName {
879+
if len(fields) == 1 {
880+
// No conflict, use the field
881+
namedFields[name] = &fields[0]
762882
continue
763883
}
764-
namedFields[fieldName] = i
884+
885+
// Find the dominant field using json/v2 precedence rules:
886+
// 1. Shallowest depth wins
887+
// 2. Among same depth, explicitly tagged field wins
888+
// 3. Among same depth with same tag status, first declared wins
889+
890+
dominant := fields[0]
891+
for i := 1; i < len(fields); i++ {
892+
candidate := fields[i]
893+
894+
// Shallowest depth wins
895+
if candidate.depth < dominant.depth {
896+
dominant = candidate
897+
continue
898+
}
899+
if candidate.depth > dominant.depth {
900+
continue
901+
}
902+
903+
// Same depth: explicitly tagged field wins
904+
if candidate.hasTag && !dominant.hasTag {
905+
dominant = candidate
906+
continue
907+
}
908+
if !candidate.hasTag && dominant.hasTag {
909+
continue
910+
}
911+
912+
// Same depth and tag status: first declared wins (keep current dominant)
913+
}
914+
915+
namedFields[name] = &dominant
765916
}
766-
fields := &fieldsType{namedFields, anonymous}
767-
fieldsMap.Store(resultType, fields)
768917

769-
return fields
918+
return &fieldsType{
919+
namedFields: namedFields,
920+
}
770921
}

0 commit comments

Comments
 (0)