@@ -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+
734752type 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
739798var 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