This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 9dd5560f6 refactor(go): move go struct serializer init to
struct_init.go (#3255)
9dd5560f6 is described below
commit 9dd5560f67e9f2aeec3d0c3512a92f2469e52e2f
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Feb 4 11:25:16 2026 +0800
refactor(go): move go struct serializer init to struct_init.go (#3255)
## Why?
- Move struct serializer initialization helpers out of `struct.go` to
keep the file focused on runtime serialization code and improve
navigation.
## What does this PR do?
- Extract struct serializer initialization helpers (hash computation,
field metadata setup, type-def alignment) into `struct_init.go`.
- Keep behavior unchanged; this is a code organization refactor.
## Related issues
#2982
## Does this PR introduce any user-facing change?
No. Refactor only; no API or protocol changes.
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
go/fory/struct.go | 1101 -----------------------------------------------
go/fory/struct_init.go | 1124 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 1124 insertions(+), 1101 deletions(-)
diff --git a/go/fory/struct.go b/go/fory/struct.go
index 957e4a1d6..123f53b7f 100644
--- a/go/fory/struct.go
+++ b/go/fory/struct.go
@@ -19,26 +19,12 @@ package fory
import (
"encoding/binary"
- "errors"
"fmt"
"math"
"reflect"
- "sort"
- "unicode"
- "unicode/utf8"
"unsafe"
)
-// GetStructHash returns the struct hash for a given type using the provided
TypeResolver.
-// This is used by codegen serializers to get the hash at runtime.
-func GetStructHash(type_ reflect.Type, resolver *TypeResolver) int32 {
- ser := newStructSerializer(type_, "")
- if err := ser.initialize(resolver); err != nil {
- panic(fmt.Errorf("failed to initialize struct serializer for
hash computation: %v", err))
- }
- return ser.structHash
-}
-
type structSerializer struct {
// Identity
name string
@@ -94,1093 +80,6 @@ func newStructSerializer(type_ reflect.Type, name string)
*structSerializer {
}
}
-// initialize performs eager initialization of the struct serializer.
-// This should be called at registration time to pre-compute all field
metadata.
-func (s *structSerializer) initialize(typeResolver *TypeResolver) error {
- if s.initialized {
- return nil
- }
- // Ensure type is set
- if s.type_ == nil {
- return errors.New("struct type not set")
- }
- // Normalize pointer types
- for s.type_.Kind() == reflect.Ptr {
- s.type_ = s.type_.Elem()
- }
- // Set compatible mode flag BEFORE field initialization
- // This is needed for groupFields to apply correct sorting
- s.isCompatibleMode = typeResolver.Compatible()
- // Build fields from type or fieldDefs
- if s.fieldDefs != nil {
- if err := s.initFieldsFromTypeDef(typeResolver); err != nil {
- return err
- }
- } else {
- if err := s.initFields(typeResolver); err != nil {
- return err
- }
- }
- // Compute struct hash
- s.structHash = s.computeHash()
- if s.tempValue == nil {
- tmp := reflect.New(s.type_).Elem()
- s.tempValue = &tmp
- }
- s.initialized = true
- return nil
-}
-
-func computeLocalNullable(typeResolver *TypeResolver, field
reflect.StructField, foryTag ForyTag) bool {
- fieldType := field.Type
- optionalInfo, isOptional := getOptionalInfo(fieldType)
- if isOptional {
- fieldType = optionalInfo.valueType
- }
- typeId := typeResolver.getTypeIdByType(fieldType)
- isEnum := typeId == ENUM
- var nullableFlag bool
- if typeResolver.fory.config.IsXlang {
- nullableFlag = isOptional || field.Type.Kind() == reflect.Ptr
- } else {
- nullableFlag = isOptional || field.Type.Kind() == reflect.Ptr ||
- field.Type.Kind() == reflect.Slice ||
- field.Type.Kind() == reflect.Map ||
- field.Type.Kind() == reflect.Interface
- }
- if foryTag.NullableSet {
- nullableFlag = foryTag.Nullable
- }
- if isNonNullablePrimitiveKind(fieldType.Kind()) && !isEnum &&
!isOptional {
- nullableFlag = false
- }
- return nullableFlag
-}
-
-func primitiveTypeIdMatchesKind(typeId TypeId, kind reflect.Kind) bool {
- switch typeId {
- case BOOL:
- return kind == reflect.Bool
- case INT8:
- return kind == reflect.Int8
- case INT16:
- return kind == reflect.Int16
- case INT32, VARINT32:
- return kind == reflect.Int32 || kind == reflect.Int
- case INT64, VARINT64, TAGGED_INT64:
- return kind == reflect.Int64 || kind == reflect.Int
- case UINT8:
- return kind == reflect.Uint8
- case UINT16:
- return kind == reflect.Uint16
- case UINT32, VAR_UINT32:
- return kind == reflect.Uint32 || kind == reflect.Uint
- case UINT64, VAR_UINT64, TAGGED_UINT64:
- return kind == reflect.Uint64 || kind == reflect.Uint
- case FLOAT32:
- return kind == reflect.Float32
- case FLOAT64:
- return kind == reflect.Float64
- case STRING:
- return kind == reflect.String
- default:
- return false
- }
-}
-
-func applyNestedRefOverride(serializer Serializer, fieldType reflect.Type,
foryTag ForyTag) Serializer {
- if !foryTag.NestedRefSet || !foryTag.NestedRefValid {
- return serializer
- }
- return applyNestedRefOverrideWithPath(serializer, fieldType,
foryTag.NestedRef)
-}
-
-func applyNestedRefOverrideWithPath(serializer Serializer, fieldType
reflect.Type, nestedRefs []bool) Serializer {
- if serializer == nil || len(nestedRefs) == 0 {
- return serializer
- }
- switch fieldType.Kind() {
- case reflect.Slice:
- if len(nestedRefs) < 1 {
- return serializer
- }
- if sliceSer, ok := serializer.(*sliceSerializer); ok {
- override := nestedRefs[0]
- newSer := *sliceSer
- newSer.referencable = newSer.referencable && override
- if len(nestedRefs) > 1 && newSer.elemSerializer != nil {
- newSer.elemSerializer =
applyNestedRefOverrideWithPath(
- newSer.elemSerializer,
- fieldType.Elem(),
- nestedRefs[1:],
- )
- }
- return &newSer
- }
- case reflect.Map:
- if len(nestedRefs) < 2 {
- return serializer
- }
- keyOverride := nestedRefs[0]
- valueOverride := nestedRefs[1]
- if mapSer, ok := serializer.(*mapSerializer); ok {
- newSer := *mapSer
- newSer.keyReferencable = newSer.keyReferencable &&
keyOverride
- newSer.valueReferencable = newSer.valueReferencable &&
valueOverride
- if len(nestedRefs) > 2 && newSer.valueSerializer != nil
{
- newSer.valueSerializer =
applyNestedRefOverrideWithPath(
- newSer.valueSerializer,
- fieldType.Elem(),
- nestedRefs[2:],
- )
- }
- return &newSer
- }
- if mapSer, ok := serializer.(mapSerializer); ok {
- mapSer.keyReferencable = mapSer.keyReferencable &&
keyOverride
- mapSer.valueReferencable = mapSer.valueReferencable &&
valueOverride
- if len(nestedRefs) > 2 && mapSer.valueSerializer != nil
{
- mapSer.valueSerializer =
applyNestedRefOverrideWithPath(
- mapSer.valueSerializer,
- fieldType.Elem(),
- nestedRefs[2:],
- )
- }
- return mapSer
- }
- }
- return serializer
-}
-
-// initFields initializes fields from local struct type using TypeResolver
-func (s *structSerializer) initFields(typeResolver *TypeResolver) error {
- // If we have fieldDefs from type_def (remote meta), use them
- if len(s.fieldDefs) > 0 {
- return s.initFieldsFromTypeDef(typeResolver)
- }
-
- // Otherwise initialize from local struct type
- type_ := s.type_
- var fields []FieldInfo
- var fieldNames []string
- var serializers []Serializer
- var typeIds []TypeId
- var nullables []bool
- var tagIDs []int
-
- for i := 0; i < type_.NumField(); i++ {
- field := type_.Field(i)
- firstRune, _ := utf8.DecodeRuneInString(field.Name)
- if unicode.IsLower(firstRune) {
- continue // skip unexported fields
- }
-
- // Parse fory struct tag and check for ignore
- foryTag := parseForyTag(field)
- if foryTag.Ignore {
- continue // skip ignored fields
- }
-
- fieldType := field.Type
- optionalInfo, isOptional := getOptionalInfo(fieldType)
- baseType := fieldType
- if isOptional {
- if err :=
validateOptionalValueType(optionalInfo.valueType); err != nil {
- return fmt.Errorf("field %s: %w", field.Name,
err)
- }
- baseType = optionalInfo.valueType
- }
- fieldKind := FieldKindValue
- if isOptional {
- fieldKind = FieldKindOptional
- } else if fieldType.Kind() == reflect.Ptr {
- fieldKind = FieldKindPointer
- }
- var fieldSerializer Serializer
- // For any fields, don't get a serializer - use
WriteValue/ReadValue instead
- // which will handle polymorphic types dynamically
- if fieldType.Kind() != reflect.Interface {
- // Get serializer for all non-interface field types
- fieldSerializer, _ =
typeResolver.getSerializerByType(fieldType, true)
- }
-
- // Use TypeResolver helper methods for arrays and slices
- if fieldType.Kind() == reflect.Array && fieldType.Elem().Kind()
!= reflect.Interface {
- fieldSerializer, _ =
typeResolver.GetArraySerializer(fieldType)
- } else if fieldType.Kind() == reflect.Slice &&
fieldType.Elem().Kind() != reflect.Interface {
- fieldSerializer, _ =
typeResolver.GetSliceSerializer(fieldType)
- } else if fieldType.Kind() == reflect.Slice &&
fieldType.Elem().Kind() == reflect.Interface {
- // For struct fields with interface element types, use
sliceDynSerializer
- fieldSerializer =
mustNewSliceDynSerializer(fieldType.Elem())
- }
- fieldSerializer = applyNestedRefOverride(fieldSerializer,
fieldType, foryTag)
-
- // Get TypeId for the serializer, fallback to deriving from kind
- fieldTypeId := typeResolver.getTypeIdByType(fieldType)
- if fieldTypeId == 0 {
- fieldTypeId = typeIdFromKind(fieldType)
- }
-
- // Override TypeId based on compress/encoding tags for integer
types
- // This matches the logic in type_def.go:buildFieldDefs
- baseKind := baseType.Kind()
- if baseKind == reflect.Ptr {
- baseKind = baseType.Elem().Kind()
- }
- switch baseKind {
- case reflect.Uint32:
- if foryTag.CompressSet {
- if foryTag.Compress {
- fieldTypeId = VAR_UINT32
- } else {
- fieldTypeId = UINT32
- }
- }
- case reflect.Int32:
- if foryTag.CompressSet {
- if foryTag.Compress {
- fieldTypeId = VARINT32
- } else {
- fieldTypeId = INT32
- }
- }
- case reflect.Uint64:
- if foryTag.EncodingSet {
- switch foryTag.Encoding {
- case "fixed":
- fieldTypeId = UINT64
- case "varint":
- fieldTypeId = VAR_UINT64
- case "tagged":
- fieldTypeId = TAGGED_UINT64
- }
- }
- case reflect.Int64:
- if foryTag.EncodingSet {
- switch foryTag.Encoding {
- case "fixed":
- fieldTypeId = INT64
- case "varint":
- fieldTypeId = VARINT64
- case "tagged":
- fieldTypeId = TAGGED_INT64
- }
- }
- }
-
- if foryTag.TypeIDSet && foryTag.TypeIDValid {
- fieldTypeId = foryTag.TypeID
- }
-
- // Calculate nullable flag for serialization (wire format):
- // - In xlang mode: Per xlang spec, fields are NON-NULLABLE by
default.
- // Only pointer types are nullable by default.
- // - In native mode: Go's natural semantics apply -
slice/map/interface can be nil,
- // so they are nullable by default.
- // Can be overridden by explicit fory tag `fory:"nullable"`.
- isEnum := fieldTypeId == ENUM
-
- // Determine nullable based on mode
- // In xlang mode: only pointer types are nullable by default
(per xlang spec)
- // In native mode: Go's natural semantics - all nil-able types
are nullable
- // This ensures proper interoperability with Java/other
languages in xlang mode.
- var nullableFlag bool
- if typeResolver.fory.config.IsXlang {
- // xlang mode: only pointer types are nullable by
default per xlang spec
- // Slices and maps are NOT nullable - they serialize as
empty when nil
- nullableFlag = isOptional || fieldType.Kind() ==
reflect.Ptr
- } else {
- // Native mode: Go's natural semantics - all nil-able
types are nullable
- nullableFlag = isOptional || fieldType.Kind() ==
reflect.Ptr ||
- fieldType.Kind() == reflect.Slice ||
- fieldType.Kind() == reflect.Map ||
- fieldType.Kind() == reflect.Interface
- }
- if foryTag.NullableSet {
- // Override nullable flag if explicitly set in fory tag
- nullableFlag = foryTag.Nullable
- }
- // Primitives are never nullable, regardless of tag
- if isNonNullablePrimitiveKind(fieldType.Kind()) && !isEnum {
- nullableFlag = false
- }
-
- // Calculate ref tracking - use tag override if explicitly set
- trackRef := typeResolver.TrackRef()
- if foryTag.RefSet {
- trackRef = foryTag.Ref
- }
- trackingRef := trackRef
- if trackingRef && !NeedWriteRef(fieldTypeId) {
- trackingRef = false
- }
- // Align trackingRef with xlang rules for field ref flags:
- // - simple value types never write ref flags
- // - collection fields only write ref flags when explicitly
tagged
- if typeResolver.fory.config.IsXlang && trackingRef &&
isCollectionType(fieldTypeId) && !foryTag.RefSet {
- trackingRef = false
- }
-
- // Pre-compute RefMode based on trackingRef and nullable.
- // When trackingRef is true, we must write ref flags even for
non-nullable fields.
- refMode := RefModeNone
- if trackingRef {
- refMode = RefModeTracking
- } else if nullableFlag {
- refMode = RefModeNullOnly
- }
- // Pre-compute WriteType: true for struct fields in compatible
mode
- writeType := typeResolver.Compatible() &&
isStructField(baseType)
-
- // Pre-compute DispatchId, with special handling for enum
fields and pointer-to-numeric
- dispatchId := getDispatchIdFromTypeId(fieldTypeId, nullableFlag)
- if dispatchId == UnknownDispatchId {
- dispatchType := baseType
- if dispatchType.Kind() == reflect.Ptr {
- dispatchType = dispatchType.Elem()
- }
- dispatchId = GetDispatchId(dispatchType)
- }
- if fieldSerializer != nil {
- if _, ok := fieldSerializer.(*enumSerializer); ok {
- dispatchId = EnumDispatchId
- } else if ptrSer, ok :=
fieldSerializer.(*ptrToValueSerializer); ok {
- if _, ok :=
ptrSer.valueSerializer.(*enumSerializer); ok {
- dispatchId = EnumDispatchId
- }
- }
- }
- if DebugOutputEnabled {
- fmt.Printf("[Go][fory-debug] initFields: field=%s
type=%v dispatchId=%d refMode=%v nullableFlag=%v serializer=%T\n",
- SnakeCase(field.Name), fieldType, dispatchId,
refMode, nullableFlag, fieldSerializer)
- }
-
- fieldInfo := FieldInfo{
- Offset: field.Offset,
- DispatchId: dispatchId,
- RefMode: refMode,
- Kind: fieldKind,
- Serializer: fieldSerializer,
- Meta: &FieldMeta{
- Name: SnakeCase(field.Name),
- Type: fieldType,
- TypeId: fieldTypeId,
- Nullable: nullableFlag, // Use same logic
as TypeDef's nullable flag for consistent ref handling
- FieldIndex: i,
- WriteType: writeType,
- HasGenerics: isCollectionType(fieldTypeId),
// Container fields have declared element types
- OptionalInfo: optionalInfo,
- TagID: foryTag.ID,
- HasForyTag: foryTag.HasTag,
- TagRefSet: foryTag.RefSet,
- TagRef: foryTag.Ref,
- TagNullableSet: foryTag.NullableSet,
- TagNullable: foryTag.Nullable,
- },
- }
- fields = append(fields, fieldInfo)
- fieldNames = append(fieldNames, fieldInfo.Meta.Name)
- serializers = append(serializers, fieldSerializer)
- typeIds = append(typeIds, fieldTypeId)
- nullables = append(nullables, nullableFlag)
- tagIDs = append(tagIDs, foryTag.ID)
- }
-
- // Sort fields according to specification using nullable info and tag
IDs for consistent ordering
- serializers, fieldNames = sortFields(typeResolver, fieldNames,
serializers, typeIds, nullables, tagIDs)
- order := make(map[string]int, len(fieldNames))
- for idx, name := range fieldNames {
- order[name] = idx
- }
-
- sort.SliceStable(fields, func(i, j int) bool {
- oi, okI := order[fields[i].Meta.Name]
- oj, okJ := order[fields[j].Meta.Name]
- switch {
- case okI && okJ:
- return oi < oj
- case okI:
- return true
- case okJ:
- return false
- default:
- return false
- }
- })
-
- s.fields = fields
- s.fieldGroup = GroupFields(s.fields)
-
- // Debug output for field order comparison with Java
- if s.type_ != nil {
- s.fieldGroup.DebugPrint(s.type_.Name())
- }
-
- return nil
-}
-
-// initFieldsFromTypeDef initializes fields from remote fieldDefs using
typeResolver
-func (s *structSerializer) initFieldsFromTypeDef(typeResolver *TypeResolver)
error {
- type_ := s.type_
- if type_ == nil {
- // Type is not known - we'll create an any placeholder
- // This happens when deserializing unknown types in compatible
mode
- // For now, we'll create fields that discard all data
- var fields []FieldInfo
- for _, def := range s.fieldDefs {
- fieldSerializer, _ :=
getFieldTypeSerializerWithResolver(typeResolver, def.fieldType)
- remoteTypeInfo, _ :=
def.fieldType.getTypeInfoWithResolver(typeResolver)
- remoteType := remoteTypeInfo.Type
- if remoteType == nil {
- remoteType = reflect.TypeOf((*any)(nil)).Elem()
- }
- // Get TypeId from FieldType's TypeId method
- fieldTypeId := def.fieldType.TypeId()
- // Pre-compute RefMode based on trackRef and FieldDef
flags
- refMode := RefModeNone
- if def.trackingRef {
- refMode = RefModeTracking
- } else if def.nullable {
- refMode = RefModeNullOnly
- }
- // Pre-compute WriteType: true for struct fields in
compatible mode
- writeType := typeResolver.Compatible() &&
isStructField(remoteType)
-
- // Pre-compute DispatchId, with special handling for
enum fields
- dispatchId := GetDispatchId(remoteType)
- if fieldSerializer != nil {
- if _, ok := fieldSerializer.(*enumSerializer);
ok {
- dispatchId = EnumDispatchId
- } else if ptrSer, ok :=
fieldSerializer.(*ptrToValueSerializer); ok {
- if _, ok :=
ptrSer.valueSerializer.(*enumSerializer); ok {
- dispatchId = EnumDispatchId
- }
- }
- }
-
- fieldInfo := FieldInfo{
- Offset: 0,
- DispatchId: dispatchId,
- RefMode: refMode,
- Kind: FieldKindValue,
- Serializer: fieldSerializer,
- Meta: &FieldMeta{
- Name: def.name,
- Type: remoteType,
- TypeId: fieldTypeId,
- Nullable: def.nullable, // Use
remote nullable flag
- FieldIndex: -1, // Mark as
non-existent field to discard data
- FieldDef: def, // Save
original FieldDef for skipping
- WriteType: writeType,
- HasGenerics:
isCollectionType(fieldTypeId), // Container fields have declared element types
- },
- }
- fields = append(fields, fieldInfo)
- }
- s.fields = fields
- s.fieldGroup = GroupFields(s.fields)
- s.typeDefDiffers = true // Unknown type, must use ordered
reading
- return nil
- }
-
- // Build maps from field names and tag IDs to struct field indices
- fieldNameToIndex := make(map[string]int)
- fieldNameToOffset := make(map[string]uintptr)
- fieldNameToType := make(map[string]reflect.Type)
- localNullableByIndex := make(map[int]bool)
- fieldTagIDToIndex := make(map[int]int) // tag ID -> struct
field index
- fieldTagIDToOffset := make(map[int]uintptr) // tag ID -> field offset
- fieldTagIDToType := make(map[int]reflect.Type) // tag ID -> field type
- fieldTagIDToName := make(map[int]string) // tag ID -> snake_case
field name
- for i := 0; i < type_.NumField(); i++ {
- field := type_.Field(i)
-
- // Parse fory tag and skip ignored fields
- foryTag := parseForyTag(field)
- if foryTag.Ignore {
- continue
- }
-
- name := SnakeCase(field.Name)
- fieldNameToIndex[name] = i
- fieldNameToOffset[name] = field.Offset
- fieldNameToType[name] = field.Type
- localNullableByIndex[i] = computeLocalNullable(typeResolver,
field, foryTag)
-
- // Also index by tag ID if present
- if foryTag.ID >= 0 {
- fieldTagIDToIndex[foryTag.ID] = i
- fieldTagIDToOffset[foryTag.ID] = field.Offset
- fieldTagIDToType[foryTag.ID] = field.Type
- fieldTagIDToName[foryTag.ID] = name
- }
- }
-
- var fields []FieldInfo
-
- for _, def := range s.fieldDefs {
- fieldSerializer, err :=
getFieldTypeSerializerWithResolver(typeResolver, def.fieldType)
- if err != nil || fieldSerializer == nil {
- // If we can't get serializer from typeID, try to get
it from the Go type
- // This can happen when the type isn't registered in
typeIDToTypeInfo
- remoteTypeInfo, _ :=
def.fieldType.getTypeInfoWithResolver(typeResolver)
- if remoteTypeInfo.Type != nil {
- fieldSerializer, _ =
typeResolver.getSerializerByType(remoteTypeInfo.Type, true)
- }
- }
-
- // Get the remote type from fieldDef
- remoteTypeInfo, _ :=
def.fieldType.getTypeInfoWithResolver(typeResolver)
- remoteType := remoteTypeInfo.Type
- // Track if type lookup failed - we'll need to skip such fields
- // Note: DynamicFieldType.getTypeInfoWithResolver returns any
(not nil) when lookup fails
- emptyInterfaceType := reflect.TypeOf((*any)(nil)).Elem()
- typeLookupFailed := remoteType == nil || remoteType ==
emptyInterfaceType
- if remoteType == nil {
- remoteType = emptyInterfaceType
- }
-
- // For struct-like fields, even if TypeDef lookup fails, we can
try to read
- // the field because type resolution happens at read time from
the buffer.
- // The type name might map to a different local type.
- isStructLikeField := isStructFieldType(def.fieldType)
-
- // Try to find corresponding local field
- // First try to match by tag ID (if remote def uses tag ID)
- // Then fall back to matching by field name
- fieldIndex := -1
- var offset uintptr
- var fieldType reflect.Type
- var localFieldName string
- var localType reflect.Type
- var exists bool
-
- if def.tagID >= 0 {
- // Try to match by tag ID
- if idx, ok := fieldTagIDToIndex[def.tagID]; ok {
- exists = true
- fieldIndex = idx // Will be overwritten if
types are compatible
- localType = fieldTagIDToType[def.tagID]
- offset = fieldTagIDToOffset[def.tagID]
- localFieldName = fieldTagIDToName[def.tagID]
- }
- }
-
- // Fall back to name-based matching if tag ID match failed
- if !exists && def.name != "" {
- if _, ok := fieldNameToIndex[def.name]; ok {
- exists = true
- localType = fieldNameToType[def.name]
- offset = fieldNameToOffset[def.name]
- localFieldName = def.name
- }
- }
-
- if exists {
- idx := fieldNameToIndex[localFieldName]
- if def.tagID >= 0 {
- idx = fieldTagIDToIndex[def.tagID]
- }
- // Check if types are compatible
- // For primitive types: skip if types don't match
- // For struct-like types: allow read even if TypeDef
lookup failed,
- // because runtime type resolution by name might work
- shouldRead := false
- isPolymorphicField := def.fieldType.TypeId() == UNKNOWN
- defTypeId := def.fieldType.TypeId()
- // Check if field is an enum - either by type ID or by
serializer type
- internalDefTypeId := defTypeId
- isEnumField := internalDefTypeId == ENUM
- if !isEnumField && fieldSerializer != nil {
- _, isEnumField =
fieldSerializer.(*enumSerializer)
- }
- if isPolymorphicField && localType.Kind() ==
reflect.Interface {
- // For polymorphic (UNKNOWN) fields with any
local type,
- // allow reading - the actual type will be
determined at runtime
- shouldRead = true
- fieldType = localType
- } else if typeLookupFailed && isEnumField {
- // For enum fields with failed TypeDef lookup,
check if local field is a numeric type
- // (Go enums are int-based)
- // Also handle pointer enum fields (*EnumType)
- localKind := localType.Kind()
- elemKind := localKind
- if localKind == reflect.Ptr {
- elemKind = localType.Elem().Kind()
- }
- if isNumericKind(elemKind) {
- shouldRead = true
- fieldType = localType
- // Get the serializer for the base type
(the enum type, not the pointer)
- baseType := localType
- if localKind == reflect.Ptr {
- baseType = localType.Elem()
- }
- fieldSerializer, _ =
typeResolver.getSerializerByType(baseType, true)
- }
- } else if typeLookupFailed && isStructLikeField {
- // For struct fields with failed TypeDef
lookup, check if local field can hold a struct
- localKind := localType.Kind()
- if localKind == reflect.Ptr {
- localKind = localType.Elem().Kind()
- }
- if localKind == reflect.Struct || localKind ==
reflect.Interface {
- shouldRead = true
- fieldType = localType // Use local type
for struct fields
- }
- } else if typeLookupFailed && (internalDefTypeId ==
UNION || internalDefTypeId == TYPED_UNION || internalDefTypeId == NAMED_UNION) {
- // For union fields with failed type lookup
(named unions aren't in typeIDToTypeInfo),
- // allow reading if the local type is a union.
- if isUnionType(localType) {
- shouldRead = true
- fieldType = localType
- }
- } else if typeLookupFailed &&
isPrimitiveType(TypeId(internalDefTypeId)) {
- baseLocal := localType
- if optInfo, ok := getOptionalInfo(baseLocal);
ok {
- baseLocal = optInfo.valueType
- }
- if baseLocal.Kind() == reflect.Ptr {
- baseLocal = baseLocal.Elem()
- }
- if
primitiveTypeIdMatchesKind(internalDefTypeId, baseLocal.Kind()) {
- shouldRead = true
- fieldType = localType
- }
- } else if typeLookupFailed &&
isPrimitiveArrayType(TypeId(internalDefTypeId)) {
- // Primitive arrays/slices use array type IDs
but may not be registered in typeIDToTypeInfo.
- // Allow reading using the local slice/array
type when the type IDs match.
- localTypeId := typeIdFromKind(localType)
- if TypeId(localTypeId&0xFF) ==
internalDefTypeId {
- shouldRead = true
- fieldType = localType
- }
- } else if typeLookupFailed && defTypeId == LIST {
- // For list fields with failed type lookup
(e.g., named struct element types),
- // allow reading using the local slice type.
- if localType.Kind() == reflect.Slice {
- elemKind := localType.Elem().Kind()
- if elemKind == reflect.Interface ||
- elemKind == reflect.Struct ||
- (elemKind == reflect.Ptr &&
localType.Elem().Elem().Kind() == reflect.Struct) {
- shouldRead = true
- fieldType = localType
- }
- }
- } else if typeLookupFailed && defTypeId == MAP {
- // For map fields with failed type lookup
(e.g., named struct key/value types),
- // allow reading using the local map type.
- if localType.Kind() == reflect.Map {
- keyKind := localType.Key().Kind()
- valueKind := localType.Elem().Kind()
- if keyKind == reflect.Interface ||
- keyKind == reflect.Struct ||
- (keyKind == reflect.Ptr &&
localType.Key().Elem().Kind() == reflect.Struct) ||
- valueKind == reflect.Interface
||
- valueKind == reflect.Struct ||
- (valueKind == reflect.Ptr &&
localType.Elem().Elem().Kind() == reflect.Struct) {
- shouldRead = true
- fieldType = localType
- }
- }
- } else if typeLookupFailed && defTypeId == SET {
- if isSetReflectType(localType) {
- shouldRead = true
- fieldType = localType
- }
- } else if defTypeId == SET &&
isSetReflectType(localType) {
- // Both remote and local are Set types, allow
reading
- shouldRead = true
- fieldType = localType
- } else if !typeLookupFailed &&
typesCompatible(localType, remoteType) {
- shouldRead = true
- fieldType = localType
- }
-
- if shouldRead {
- fieldIndex = idx
- // offset was already set above when matching
by tag ID or field name
- // For struct-like fields with failed type
lookup, get the serializer for the local type
- if typeLookupFailed && isStructLikeField &&
fieldSerializer == nil {
- fieldSerializer, _ =
typeResolver.getSerializerByType(localType, true)
- }
- // For collection fields with interface element
types, use sliceDynSerializer
- if typeLookupFailed && (defTypeId == LIST ||
defTypeId == SET) && fieldSerializer == nil {
- if localType.Kind() == reflect.Slice &&
localType.Elem().Kind() == reflect.Interface {
- fieldSerializer =
mustNewSliceDynSerializer(localType.Elem())
- }
- }
- // If serializer is still nil, fall back to
local type serializer.
- if fieldSerializer == nil {
- fieldSerializer, _ =
typeResolver.getSerializerByType(localType, true)
- }
- // For Set fields (fory.Set[T] =
map[T]struct{}), get the setSerializer
- if defTypeId == SET &&
isSetReflectType(localType) && fieldSerializer == nil {
- fieldSerializer, _ =
typeResolver.getSerializerByType(localType, true)
- }
- // If local type is *T and remote type is T, we
need the serializer for *T
- // This handles Java's Integer/Long (nullable
boxed types) mapping to Go's *int32/*int64
- if localType.Kind() == reflect.Ptr &&
localType.Elem() == remoteType {
- fieldSerializer, _ =
typeResolver.getSerializerByType(localType, true)
- }
- // For pointer enum fields (*EnumType), get the
serializer for the base enum type
- // The struct read/write code will handle
pointer dereferencing
- if isEnumField && localType.Kind() ==
reflect.Ptr {
- baseType := localType.Elem()
- fieldSerializer, _ =
typeResolver.getSerializerByType(baseType, true)
- if DebugOutputEnabled {
- fmt.Printf("[fory-debug]
pointer enum field %s: localType=%v baseType=%v serializer=%T\n",
- def.name, localType,
baseType, fieldSerializer)
- }
- }
- // For array fields, use array serializers (not
slice serializers) even if typeID maps to slice serializer
- // The typeID (INT16_ARRAY, etc.) is shared
between arrays and slices, but we need the correct
- // serializer based on the actual Go type
- if localType.Kind() == reflect.Array {
- elemType := localType.Elem()
- switch elemType.Kind() {
- case reflect.Bool:
- fieldSerializer =
boolArraySerializer{arrayType: localType}
- case reflect.Int8:
- fieldSerializer =
int8ArraySerializer{arrayType: localType}
- case reflect.Int16:
- fieldSerializer =
int16ArraySerializer{arrayType: localType}
- case reflect.Int32:
- fieldSerializer =
int32ArraySerializer{arrayType: localType}
- case reflect.Int64:
- fieldSerializer =
int64ArraySerializer{arrayType: localType}
- case reflect.Uint8:
- fieldSerializer =
uint8ArraySerializer{arrayType: localType}
- case reflect.Float32:
- fieldSerializer =
float32ArraySerializer{arrayType: localType}
- case reflect.Float64:
- fieldSerializer =
float64ArraySerializer{arrayType: localType}
- case reflect.Int:
- if
reflect.TypeOf(int(0)).Size() == 8 {
- fieldSerializer =
int64ArraySerializer{arrayType: localType}
- } else {
- fieldSerializer =
int32ArraySerializer{arrayType: localType}
- }
- }
- }
- } else {
- // Types are incompatible or unknown - use
remote type but mark field as not settable
- fieldType = remoteType
- fieldIndex = -1
- offset = 0 // Don't set offset for incompatible
fields
- }
- } else {
- // Field doesn't exist locally, use type from fieldDef
- fieldType = remoteType
- }
-
- optionalInfo, isOptional := getOptionalInfo(fieldType)
- baseType := fieldType
- if isOptional {
- if err :=
validateOptionalValueType(optionalInfo.valueType); err != nil {
- return fmt.Errorf("field %s: %w", def.name, err)
- }
- baseType = optionalInfo.valueType
- }
- fieldKind := FieldKindValue
- if isOptional {
- fieldKind = FieldKindOptional
- } else if fieldType.Kind() == reflect.Ptr {
- fieldKind = FieldKindPointer
- }
- if fieldKind == FieldKindOptional {
- // Use the Optional serializer for local Optional[T]
fields.
- // The serializer resolved from remote type IDs is for
the element type.
- fieldSerializer, _ =
typeResolver.getSerializerByType(fieldType, true)
- }
-
- // Get TypeId from FieldType's TypeId method
- fieldTypeId := def.fieldType.TypeId()
- // Pre-compute RefMode based on FieldDef flags (trackingRef and
nullable)
- refMode := RefModeNone
- if def.trackingRef {
- refMode = RefModeTracking
- } else if def.nullable {
- refMode = RefModeNullOnly
- }
- // Pre-compute WriteType: true for struct fields in compatible
mode
- writeType := typeResolver.Compatible() &&
isStructField(baseType)
-
- // Pre-compute DispatchId, with special handling for
pointer-to-numeric and enum fields
- // IMPORTANT: For compatible mode reading, we must use the
REMOTE nullable flag
- // to determine DispatchId, because Java wrote data with its
nullable semantics.
- var dispatchId DispatchId
- localKind := fieldType.Kind()
- baseKind := localKind
- if isOptional {
- baseKind = baseType.Kind()
- }
- localIsPtr := localKind == reflect.Ptr
- localIsPrimitive := isPrimitiveDispatchKind(baseKind) ||
(localIsPtr && isPrimitiveDispatchKind(fieldType.Elem().Kind()))
-
- if localIsPrimitive {
- if def.nullable {
- // Remote is nullable - use nullable DispatchId
- dispatchId =
getDispatchIdFromTypeId(fieldTypeId, true)
- } else {
- // Remote is NOT nullable - use primitive
DispatchId
- dispatchId =
getDispatchIdFromTypeId(fieldTypeId, false)
- if dispatchId == UnknownDispatchId {
- dispatchType := baseType
- if dispatchType.Kind() == reflect.Ptr {
- dispatchType =
dispatchType.Elem()
- }
- dispatchId = GetDispatchId(dispatchType)
- }
- }
- } else {
- dispatchType := baseType
- if dispatchType.Kind() == reflect.Ptr {
- dispatchType = dispatchType.Elem()
- }
- dispatchId = GetDispatchId(dispatchType)
- }
- if fieldSerializer != nil {
- if _, ok := fieldSerializer.(*enumSerializer); ok {
- dispatchId = EnumDispatchId
- } else if ptrSer, ok :=
fieldSerializer.(*ptrToValueSerializer); ok {
- if _, ok :=
ptrSer.valueSerializer.(*enumSerializer); ok {
- dispatchId = EnumDispatchId
- }
- }
- }
-
- // Determine field name: use local field name if matched,
otherwise use def.name
- fieldName := def.name
- if localFieldName != "" {
- fieldName = localFieldName
- }
-
- fieldInfo := FieldInfo{
- Offset: offset,
- DispatchId: dispatchId,
- RefMode: refMode,
- Kind: fieldKind,
- Serializer: fieldSerializer,
- Meta: &FieldMeta{
- Name: fieldName,
- Type: fieldType,
- TypeId: fieldTypeId,
- Nullable: def.nullable, // Use remote
nullable flag
- FieldIndex: fieldIndex,
- FieldDef: def, // Save original FieldDef
for skipping
- WriteType: writeType,
- HasGenerics: isCollectionType(fieldTypeId), //
Container fields have declared element types
- OptionalInfo: optionalInfo,
- TagID: def.tagID,
- HasForyTag: def.tagID >= 0,
- },
- }
- fields = append(fields, fieldInfo)
- }
-
- s.fields = fields
- s.fieldGroup = GroupFields(s.fields)
-
- // Debug output for field order comparison with Java
MetaSharedSerializer
- if DebugOutputEnabled && s.type_ != nil {
- fmt.Printf("[Go] Remote TypeDef order (%d fields):\n",
len(s.fieldDefs))
- for i, def := range s.fieldDefs {
- fmt.Printf("[Go] [%d] %s -> typeId=%d,
nullable=%v\n", i, def.name, def.fieldType.TypeId(), def.nullable)
- }
- s.fieldGroup.DebugPrint(s.type_.Name())
- }
-
- // Compute typeDefDiffers: true if any field doesn't exist locally, has
type mismatch,
- // or has nullable mismatch (which affects field ordering)
- // When typeDefDiffers is false, we can use grouped reading for better
performance
- s.typeDefDiffers = false
- for i, field := range fields {
- if field.Meta.FieldIndex < 0 {
- // Field exists in remote TypeDef but not locally
- if DebugOutputEnabled && s.type_ != nil {
- fmt.Printf("[Go][fory-debug] [%s]
typeDefDiffers: missing local field for remote def idx=%d name=%q tagID=%d
typeId=%d\n",
- s.name, i, s.fieldDefs[i].name,
s.fieldDefs[i].tagID, s.fieldDefs[i].fieldType.TypeId())
- }
- s.typeDefDiffers = true
- break
- }
- // Check if nullable flag differs between remote and local
- // Remote nullable is stored in fieldDefs[i].nullable
- // Local nullable is determined by whether the Go field is a
pointer type
- if i < len(s.fieldDefs) && field.Meta.FieldIndex >= 0 {
- remoteNullable := s.fieldDefs[i].nullable
- // Check if local Go field is nullable based on local
field definitions
- localNullable :=
localNullableByIndex[field.Meta.FieldIndex]
- if remoteNullable != localNullable {
- if DebugOutputEnabled && s.type_ != nil {
- fmt.Printf("[Go][fory-debug] [%s]
typeDefDiffers: nullable mismatch idx=%d name=%q tagID=%d remote=%v local=%v\n",
- s.name, i, s.fieldDefs[i].name,
s.fieldDefs[i].tagID, remoteNullable, localNullable)
- }
- s.typeDefDiffers = true
- break
- }
- remoteTypeId :=
TypeId(s.fieldDefs[i].fieldType.TypeId())
- localTypeId :=
typeResolver.getTypeIdByType(field.Meta.Type)
- if localTypeId == 0 {
- localTypeId = typeIdFromKind(field.Meta.Type)
- }
- localTypeId = TypeId(localTypeId)
- if !typeIdEqualForDiff(remoteTypeId, localTypeId) {
- if DebugOutputEnabled && s.type_ != nil {
- fmt.Printf("[Go][fory-debug] [%s]
typeDefDiffers: type ID mismatch idx=%d name=%q tagID=%d remote=%d local=%d\n",
- s.name, i, s.fieldDefs[i].name,
s.fieldDefs[i].tagID, remoteTypeId, localTypeId)
- }
- s.typeDefDiffers = true
- break
- }
- }
- }
-
- if DebugOutputEnabled && s.type_ != nil {
- fmt.Printf("[Go] typeDefDiffers=%v for %s\n", s.typeDefDiffers,
s.type_.Name())
- }
-
- return nil
-}
-
-func typeIdEqualForDiff(remoteTypeId TypeId, localTypeId TypeId) bool {
- if remoteTypeId == localTypeId {
- return true
- }
- if remoteTypeId == UNION && (localTypeId == TYPED_UNION || localTypeId
== NAMED_UNION) {
- return true
- }
- if localTypeId == UNION && (remoteTypeId == TYPED_UNION || remoteTypeId
== NAMED_UNION) {
- return true
- }
- // Treat byte array encodings as compatible for diffing.
- if (remoteTypeId == INT8_ARRAY || remoteTypeId == UINT8_ARRAY ||
remoteTypeId == BINARY) &&
- (localTypeId == INT8_ARRAY || localTypeId == UINT8_ARRAY ||
localTypeId == BINARY) {
- return true
- }
- return false
-}
-
-func (s *structSerializer) computeHash() int32 {
- // Build FieldFingerprintInfo for each field
- fields := make([]FieldFingerprintInfo, 0, len(s.fields))
- for _, field := range s.fields {
- var typeId TypeId
- isEnumField := false
- if field.Serializer == nil {
- typeId = UNKNOWN
- } else {
- typeId = field.Meta.TypeId
- // Check if this is an enum serializer (directly or
wrapped in ptrToValueSerializer)
- if _, ok := field.Serializer.(*enumSerializer); ok {
- isEnumField = true
- typeId = UNKNOWN
- } else if ptrSer, ok :=
field.Serializer.(*ptrToValueSerializer); ok {
- if _, ok :=
ptrSer.valueSerializer.(*enumSerializer); ok {
- isEnumField = true
- typeId = UNKNOWN
- }
- }
- // Unions use UNION type ID in fingerprints, regardless
of typed/named variants.
- if typeId == TYPED_UNION || typeId == NAMED_UNION ||
typeId == UNION {
- typeId = UNION
- }
- // For user-defined types (struct, ext types), use
UNKNOWN in fingerprint
- // This matches Java's behavior where user-defined
types return UNKNOWN
- // to ensure consistent fingerprint computation across
languages
- if isUserDefinedType(typeId) {
- typeId = UNKNOWN
- }
- fieldTypeForHash := field.Meta.Type
- if field.Kind == FieldKindOptional {
- fieldTypeForHash =
field.Meta.OptionalInfo.valueType
- }
- // For fixed-size arrays with primitive elements, use
primitive array type IDs
- if fieldTypeForHash.Kind() == reflect.Array {
- elemKind := fieldTypeForHash.Elem().Kind()
- switch elemKind {
- case reflect.Int8:
- typeId = INT8_ARRAY
- case reflect.Uint8:
- typeId = UINT8_ARRAY
- case reflect.Int16:
- typeId = INT16_ARRAY
- case reflect.Uint16:
- typeId = UINT16_ARRAY
- case reflect.Int32:
- typeId = INT32_ARRAY
- case reflect.Uint32:
- typeId = UINT32_ARRAY
- case reflect.Int64:
- typeId = INT64_ARRAY
- case reflect.Uint64, reflect.Uint:
- typeId = UINT64_ARRAY
- case reflect.Float32:
- typeId = FLOAT32_ARRAY
- case reflect.Float64:
- typeId = FLOAT64_ARRAY
- default:
- typeId = LIST
- }
- } else if fieldTypeForHash.Kind() == reflect.Slice {
- if !isPrimitiveArrayType(TypeId(typeId)) &&
typeId != BINARY {
- typeId = LIST
- }
- } else if fieldTypeForHash.Kind() == reflect.Map {
- // fory.Set[T] is defined as map[T]struct{} -
check for struct{} elem type
- if isSetReflectType(fieldTypeForHash) {
- typeId = SET
- } else {
- typeId = MAP
- }
- }
- }
-
- // Determine nullable flag for xlang compatibility:
- // - Default: false for ALL fields (xlang default - aligned
with all languages)
- // - Primitives are always non-nullable
- // - Can be overridden by explicit fory tag
- nullable := field.Kind == FieldKindOptional // Optional fields
are nullable by default
- if field.Meta.TagNullableSet {
- // Use explicit tag value if set
- nullable = field.Meta.TagNullable
- }
- // Primitives are never nullable, regardless of tag
- fieldTypeForNullable := field.Meta.Type
- if field.Kind == FieldKindOptional {
- fieldTypeForNullable = field.Meta.OptionalInfo.valueType
- }
- if field.Kind != FieldKindOptional &&
isNonNullablePrimitiveKind(fieldTypeForNullable.Kind()) && !isEnumField {
- nullable = false
- }
-
- fields = append(fields, FieldFingerprintInfo{
- FieldID: field.Meta.TagID,
- FieldName: SnakeCase(field.Meta.Name),
- TypeID: typeId,
- // Ref is based on explicit tag annotation only, NOT
runtime track_ref config
- // This allows fingerprint to be computed at compile
time for C++/Rust
- Ref: field.Meta.TagRefSet && field.Meta.TagRef,
- Nullable: nullable,
- })
- }
-
- hashString := ComputeStructFingerprint(fields)
- data := []byte(hashString)
- h1, _ := Murmur3Sum128WithSeed(data, 47)
- hash := int32(h1 & 0xFFFFFFFF)
-
- if DebugOutputEnabled {
- fmt.Printf("[Go][fory-debug] struct %v version
fingerprint=\"%s\" version hash=%d\n", s.type_, hashString, hash)
- }
-
- if hash == 0 {
- panic(fmt.Errorf("hash for type %v is 0", s.type_))
- }
- return hash
-}
-
func (s *structSerializer) Write(ctx *WriteContext, refMode RefMode, writeType
bool, hasGenerics bool, value reflect.Value) {
switch refMode {
case RefModeTracking:
diff --git a/go/fory/struct_init.go b/go/fory/struct_init.go
new file mode 100644
index 000000000..8dc8eae5a
--- /dev/null
+++ b/go/fory/struct_init.go
@@ -0,0 +1,1124 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package fory
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "sort"
+ "unicode"
+ "unicode/utf8"
+)
+
+// GetStructHash returns the struct hash for a given type using the provided
TypeResolver.
+// This is used by codegen serializers to get the hash at runtime.
+func GetStructHash(type_ reflect.Type, resolver *TypeResolver) int32 {
+ ser := newStructSerializer(type_, "")
+ if err := ser.initialize(resolver); err != nil {
+ panic(fmt.Errorf("failed to initialize struct serializer for
hash computation: %v", err))
+ }
+ return ser.structHash
+}
+
+// initialize performs eager initialization of the struct serializer.
+// This should be called at registration time to pre-compute all field
metadata.
+func (s *structSerializer) initialize(typeResolver *TypeResolver) error {
+ if s.initialized {
+ return nil
+ }
+ // Ensure type is set
+ if s.type_ == nil {
+ return errors.New("struct type not set")
+ }
+ // Normalize pointer types
+ for s.type_.Kind() == reflect.Ptr {
+ s.type_ = s.type_.Elem()
+ }
+ // Set compatible mode flag BEFORE field initialization
+ // This is needed for groupFields to apply correct sorting
+ s.isCompatibleMode = typeResolver.Compatible()
+ // Build fields from type or fieldDefs
+ if s.fieldDefs != nil {
+ if err := s.initFieldsFromTypeDef(typeResolver); err != nil {
+ return err
+ }
+ } else {
+ if err := s.initFields(typeResolver); err != nil {
+ return err
+ }
+ }
+ // Compute struct hash
+ s.structHash = s.computeHash()
+ if s.tempValue == nil {
+ tmp := reflect.New(s.type_).Elem()
+ s.tempValue = &tmp
+ }
+ s.initialized = true
+ return nil
+}
+
+func computeLocalNullable(typeResolver *TypeResolver, field
reflect.StructField, foryTag ForyTag) bool {
+ fieldType := field.Type
+ optionalInfo, isOptional := getOptionalInfo(fieldType)
+ if isOptional {
+ fieldType = optionalInfo.valueType
+ }
+ typeId := typeResolver.getTypeIdByType(fieldType)
+ isEnum := typeId == ENUM
+ var nullableFlag bool
+ if typeResolver.fory.config.IsXlang {
+ nullableFlag = isOptional || field.Type.Kind() == reflect.Ptr
+ } else {
+ nullableFlag = isOptional || field.Type.Kind() == reflect.Ptr ||
+ field.Type.Kind() == reflect.Slice ||
+ field.Type.Kind() == reflect.Map ||
+ field.Type.Kind() == reflect.Interface
+ }
+ if foryTag.NullableSet {
+ nullableFlag = foryTag.Nullable
+ }
+ if isNonNullablePrimitiveKind(fieldType.Kind()) && !isEnum &&
!isOptional {
+ nullableFlag = false
+ }
+ return nullableFlag
+}
+
+func primitiveTypeIdMatchesKind(typeId TypeId, kind reflect.Kind) bool {
+ switch typeId {
+ case BOOL:
+ return kind == reflect.Bool
+ case INT8:
+ return kind == reflect.Int8
+ case INT16:
+ return kind == reflect.Int16
+ case INT32, VARINT32:
+ return kind == reflect.Int32 || kind == reflect.Int
+ case INT64, VARINT64, TAGGED_INT64:
+ return kind == reflect.Int64 || kind == reflect.Int
+ case UINT8:
+ return kind == reflect.Uint8
+ case UINT16:
+ return kind == reflect.Uint16
+ case UINT32, VAR_UINT32:
+ return kind == reflect.Uint32 || kind == reflect.Uint
+ case UINT64, VAR_UINT64, TAGGED_UINT64:
+ return kind == reflect.Uint64 || kind == reflect.Uint
+ case FLOAT32:
+ return kind == reflect.Float32
+ case FLOAT64:
+ return kind == reflect.Float64
+ case STRING:
+ return kind == reflect.String
+ default:
+ return false
+ }
+}
+
+func applyNestedRefOverride(serializer Serializer, fieldType reflect.Type,
foryTag ForyTag) Serializer {
+ if !foryTag.NestedRefSet || !foryTag.NestedRefValid {
+ return serializer
+ }
+ return applyNestedRefOverrideWithPath(serializer, fieldType,
foryTag.NestedRef)
+}
+
+func applyNestedRefOverrideWithPath(serializer Serializer, fieldType
reflect.Type, nestedRefs []bool) Serializer {
+ if serializer == nil || len(nestedRefs) == 0 {
+ return serializer
+ }
+ switch fieldType.Kind() {
+ case reflect.Slice:
+ if len(nestedRefs) < 1 {
+ return serializer
+ }
+ if sliceSer, ok := serializer.(*sliceSerializer); ok {
+ override := nestedRefs[0]
+ newSer := *sliceSer
+ newSer.referencable = newSer.referencable && override
+ if len(nestedRefs) > 1 && newSer.elemSerializer != nil {
+ newSer.elemSerializer =
applyNestedRefOverrideWithPath(
+ newSer.elemSerializer,
+ fieldType.Elem(),
+ nestedRefs[1:],
+ )
+ }
+ return &newSer
+ }
+ case reflect.Map:
+ if len(nestedRefs) < 2 {
+ return serializer
+ }
+ keyOverride := nestedRefs[0]
+ valueOverride := nestedRefs[1]
+ if mapSer, ok := serializer.(*mapSerializer); ok {
+ newSer := *mapSer
+ newSer.keyReferencable = newSer.keyReferencable &&
keyOverride
+ newSer.valueReferencable = newSer.valueReferencable &&
valueOverride
+ if len(nestedRefs) > 2 && newSer.valueSerializer != nil
{
+ newSer.valueSerializer =
applyNestedRefOverrideWithPath(
+ newSer.valueSerializer,
+ fieldType.Elem(),
+ nestedRefs[2:],
+ )
+ }
+ return &newSer
+ }
+ if mapSer, ok := serializer.(mapSerializer); ok {
+ mapSer.keyReferencable = mapSer.keyReferencable &&
keyOverride
+ mapSer.valueReferencable = mapSer.valueReferencable &&
valueOverride
+ if len(nestedRefs) > 2 && mapSer.valueSerializer != nil
{
+ mapSer.valueSerializer =
applyNestedRefOverrideWithPath(
+ mapSer.valueSerializer,
+ fieldType.Elem(),
+ nestedRefs[2:],
+ )
+ }
+ return mapSer
+ }
+ }
+ return serializer
+}
+
+// initFields initializes fields from local struct type using TypeResolver
+func (s *structSerializer) initFields(typeResolver *TypeResolver) error {
+ // If we have fieldDefs from type_def (remote meta), use them
+ if len(s.fieldDefs) > 0 {
+ return s.initFieldsFromTypeDef(typeResolver)
+ }
+
+ // Otherwise initialize from local struct type
+ type_ := s.type_
+ var fields []FieldInfo
+ var fieldNames []string
+ var serializers []Serializer
+ var typeIds []TypeId
+ var nullables []bool
+ var tagIDs []int
+
+ for i := 0; i < type_.NumField(); i++ {
+ field := type_.Field(i)
+ firstRune, _ := utf8.DecodeRuneInString(field.Name)
+ if unicode.IsLower(firstRune) {
+ continue // skip unexported fields
+ }
+
+ // Parse fory struct tag and check for ignore
+ foryTag := parseForyTag(field)
+ if foryTag.Ignore {
+ continue // skip ignored fields
+ }
+
+ fieldType := field.Type
+ optionalInfo, isOptional := getOptionalInfo(fieldType)
+ baseType := fieldType
+ if isOptional {
+ if err :=
validateOptionalValueType(optionalInfo.valueType); err != nil {
+ return fmt.Errorf("field %s: %w", field.Name,
err)
+ }
+ baseType = optionalInfo.valueType
+ }
+ fieldKind := FieldKindValue
+ if isOptional {
+ fieldKind = FieldKindOptional
+ } else if fieldType.Kind() == reflect.Ptr {
+ fieldKind = FieldKindPointer
+ }
+ var fieldSerializer Serializer
+ // For any fields, don't get a serializer - use
WriteValue/ReadValue instead
+ // which will handle polymorphic types dynamically
+ if fieldType.Kind() != reflect.Interface {
+ // Get serializer for all non-interface field types
+ fieldSerializer, _ =
typeResolver.getSerializerByType(fieldType, true)
+ }
+
+ // Use TypeResolver helper methods for arrays and slices
+ if fieldType.Kind() == reflect.Array && fieldType.Elem().Kind()
!= reflect.Interface {
+ fieldSerializer, _ =
typeResolver.GetArraySerializer(fieldType)
+ } else if fieldType.Kind() == reflect.Slice &&
fieldType.Elem().Kind() != reflect.Interface {
+ fieldSerializer, _ =
typeResolver.GetSliceSerializer(fieldType)
+ } else if fieldType.Kind() == reflect.Slice &&
fieldType.Elem().Kind() == reflect.Interface {
+ // For struct fields with interface element types, use
sliceDynSerializer
+ fieldSerializer =
mustNewSliceDynSerializer(fieldType.Elem())
+ }
+ fieldSerializer = applyNestedRefOverride(fieldSerializer,
fieldType, foryTag)
+
+ // Get TypeId for the serializer, fallback to deriving from kind
+ fieldTypeId := typeResolver.getTypeIdByType(fieldType)
+ if fieldTypeId == 0 {
+ fieldTypeId = typeIdFromKind(fieldType)
+ }
+
+ // Override TypeId based on compress/encoding tags for integer
types
+ // This matches the logic in type_def.go:buildFieldDefs
+ baseKind := baseType.Kind()
+ if baseKind == reflect.Ptr {
+ baseKind = baseType.Elem().Kind()
+ }
+ switch baseKind {
+ case reflect.Uint32:
+ if foryTag.CompressSet {
+ if foryTag.Compress {
+ fieldTypeId = VAR_UINT32
+ } else {
+ fieldTypeId = UINT32
+ }
+ }
+ case reflect.Int32:
+ if foryTag.CompressSet {
+ if foryTag.Compress {
+ fieldTypeId = VARINT32
+ } else {
+ fieldTypeId = INT32
+ }
+ }
+ case reflect.Uint64:
+ if foryTag.EncodingSet {
+ switch foryTag.Encoding {
+ case "fixed":
+ fieldTypeId = UINT64
+ case "varint":
+ fieldTypeId = VAR_UINT64
+ case "tagged":
+ fieldTypeId = TAGGED_UINT64
+ }
+ }
+ case reflect.Int64:
+ if foryTag.EncodingSet {
+ switch foryTag.Encoding {
+ case "fixed":
+ fieldTypeId = INT64
+ case "varint":
+ fieldTypeId = VARINT64
+ case "tagged":
+ fieldTypeId = TAGGED_INT64
+ }
+ }
+ }
+
+ if foryTag.TypeIDSet && foryTag.TypeIDValid {
+ fieldTypeId = foryTag.TypeID
+ }
+
+ // Calculate nullable flag for serialization (wire format):
+ // - In xlang mode: Per xlang spec, fields are NON-NULLABLE by
default.
+ // Only pointer types are nullable by default.
+ // - In native mode: Go's natural semantics apply -
slice/map/interface can be nil,
+ // so they are nullable by default.
+ // Can be overridden by explicit fory tag `fory:"nullable"`.
+ isEnum := fieldTypeId == ENUM
+
+ // Determine nullable based on mode
+ // In xlang mode: only pointer types are nullable by default
(per xlang spec)
+ // In native mode: Go's natural semantics - all nil-able types
are nullable
+ // This ensures proper interoperability with Java/other
languages in xlang mode.
+ var nullableFlag bool
+ if typeResolver.fory.config.IsXlang {
+ // xlang mode: only pointer types are nullable by
default per xlang spec
+ // Slices and maps are NOT nullable - they serialize as
empty when nil
+ nullableFlag = isOptional || fieldType.Kind() ==
reflect.Ptr
+ } else {
+ // Native mode: Go's natural semantics - all nil-able
types are nullable
+ nullableFlag = isOptional || fieldType.Kind() ==
reflect.Ptr ||
+ fieldType.Kind() == reflect.Slice ||
+ fieldType.Kind() == reflect.Map ||
+ fieldType.Kind() == reflect.Interface
+ }
+ if foryTag.NullableSet {
+ // Override nullable flag if explicitly set in fory tag
+ nullableFlag = foryTag.Nullable
+ }
+ // Primitives are never nullable, regardless of tag
+ if isNonNullablePrimitiveKind(fieldType.Kind()) && !isEnum {
+ nullableFlag = false
+ }
+
+ // Calculate ref tracking - use tag override if explicitly set
+ trackRef := typeResolver.TrackRef()
+ if foryTag.RefSet {
+ trackRef = foryTag.Ref
+ }
+ trackingRef := trackRef
+ if trackingRef && !NeedWriteRef(fieldTypeId) {
+ trackingRef = false
+ }
+ // Align trackingRef with xlang rules for field ref flags:
+ // - simple value types never write ref flags
+ // - collection fields only write ref flags when explicitly
tagged
+ if typeResolver.fory.config.IsXlang && trackingRef &&
isCollectionType(fieldTypeId) && !foryTag.RefSet {
+ trackingRef = false
+ }
+
+ // Pre-compute RefMode based on trackingRef and nullable.
+ // When trackingRef is true, we must write ref flags even for
non-nullable fields.
+ refMode := RefModeNone
+ if trackingRef {
+ refMode = RefModeTracking
+ } else if nullableFlag {
+ refMode = RefModeNullOnly
+ }
+ // Pre-compute WriteType: true for struct fields in compatible
mode
+ writeType := typeResolver.Compatible() &&
isStructField(baseType)
+
+ // Pre-compute DispatchId, with special handling for enum
fields and pointer-to-numeric
+ dispatchId := getDispatchIdFromTypeId(fieldTypeId, nullableFlag)
+ if dispatchId == UnknownDispatchId {
+ dispatchType := baseType
+ if dispatchType.Kind() == reflect.Ptr {
+ dispatchType = dispatchType.Elem()
+ }
+ dispatchId = GetDispatchId(dispatchType)
+ }
+ if fieldSerializer != nil {
+ if _, ok := fieldSerializer.(*enumSerializer); ok {
+ dispatchId = EnumDispatchId
+ } else if ptrSer, ok :=
fieldSerializer.(*ptrToValueSerializer); ok {
+ if _, ok :=
ptrSer.valueSerializer.(*enumSerializer); ok {
+ dispatchId = EnumDispatchId
+ }
+ }
+ }
+ if DebugOutputEnabled {
+ fmt.Printf("[Go][fory-debug] initFields: field=%s
type=%v dispatchId=%d refMode=%v nullableFlag=%v serializer=%T\n",
+ SnakeCase(field.Name), fieldType, dispatchId,
refMode, nullableFlag, fieldSerializer)
+ }
+
+ fieldInfo := FieldInfo{
+ Offset: field.Offset,
+ DispatchId: dispatchId,
+ RefMode: refMode,
+ Kind: fieldKind,
+ Serializer: fieldSerializer,
+ Meta: &FieldMeta{
+ Name: SnakeCase(field.Name),
+ Type: fieldType,
+ TypeId: fieldTypeId,
+ Nullable: nullableFlag, // Use same logic
as TypeDef's nullable flag for consistent ref handling
+ FieldIndex: i,
+ WriteType: writeType,
+ HasGenerics: isCollectionType(fieldTypeId),
// Container fields have declared element types
+ OptionalInfo: optionalInfo,
+ TagID: foryTag.ID,
+ HasForyTag: foryTag.HasTag,
+ TagRefSet: foryTag.RefSet,
+ TagRef: foryTag.Ref,
+ TagNullableSet: foryTag.NullableSet,
+ TagNullable: foryTag.Nullable,
+ },
+ }
+ fields = append(fields, fieldInfo)
+ fieldNames = append(fieldNames, fieldInfo.Meta.Name)
+ serializers = append(serializers, fieldSerializer)
+ typeIds = append(typeIds, fieldTypeId)
+ nullables = append(nullables, nullableFlag)
+ tagIDs = append(tagIDs, foryTag.ID)
+ }
+
+ // Sort fields according to specification using nullable info and tag
IDs for consistent ordering
+ serializers, fieldNames = sortFields(typeResolver, fieldNames,
serializers, typeIds, nullables, tagIDs)
+ order := make(map[string]int, len(fieldNames))
+ for idx, name := range fieldNames {
+ order[name] = idx
+ }
+
+ sort.SliceStable(fields, func(i, j int) bool {
+ oi, okI := order[fields[i].Meta.Name]
+ oj, okJ := order[fields[j].Meta.Name]
+ switch {
+ case okI && okJ:
+ return oi < oj
+ case okI:
+ return true
+ case okJ:
+ return false
+ default:
+ return false
+ }
+ })
+
+ s.fields = fields
+ s.fieldGroup = GroupFields(s.fields)
+
+ // Debug output for field order comparison with Java
+ if s.type_ != nil {
+ s.fieldGroup.DebugPrint(s.type_.Name())
+ }
+
+ return nil
+}
+
+// initFieldsFromTypeDef initializes fields from remote fieldDefs using
typeResolver
+func (s *structSerializer) initFieldsFromTypeDef(typeResolver *TypeResolver)
error {
+ type_ := s.type_
+ if type_ == nil {
+ // Type is not known - we'll create an any placeholder
+ // This happens when deserializing unknown types in compatible
mode
+ // For now, we'll create fields that discard all data
+ var fields []FieldInfo
+ for _, def := range s.fieldDefs {
+ fieldSerializer, _ :=
getFieldTypeSerializerWithResolver(typeResolver, def.fieldType)
+ remoteTypeInfo, _ :=
def.fieldType.getTypeInfoWithResolver(typeResolver)
+ remoteType := remoteTypeInfo.Type
+ if remoteType == nil {
+ remoteType = reflect.TypeOf((*any)(nil)).Elem()
+ }
+ // Get TypeId from FieldType's TypeId method
+ fieldTypeId := def.fieldType.TypeId()
+ // Pre-compute RefMode based on trackRef and FieldDef
flags
+ refMode := RefModeNone
+ if def.trackingRef {
+ refMode = RefModeTracking
+ } else if def.nullable {
+ refMode = RefModeNullOnly
+ }
+ // Pre-compute WriteType: true for struct fields in
compatible mode
+ writeType := typeResolver.Compatible() &&
isStructField(remoteType)
+
+ // Pre-compute DispatchId, with special handling for
enum fields
+ dispatchId := GetDispatchId(remoteType)
+ if fieldSerializer != nil {
+ if _, ok := fieldSerializer.(*enumSerializer);
ok {
+ dispatchId = EnumDispatchId
+ } else if ptrSer, ok :=
fieldSerializer.(*ptrToValueSerializer); ok {
+ if _, ok :=
ptrSer.valueSerializer.(*enumSerializer); ok {
+ dispatchId = EnumDispatchId
+ }
+ }
+ }
+
+ fieldInfo := FieldInfo{
+ Offset: 0,
+ DispatchId: dispatchId,
+ RefMode: refMode,
+ Kind: FieldKindValue,
+ Serializer: fieldSerializer,
+ Meta: &FieldMeta{
+ Name: def.name,
+ Type: remoteType,
+ TypeId: fieldTypeId,
+ Nullable: def.nullable, // Use
remote nullable flag
+ FieldIndex: -1, // Mark as
non-existent field to discard data
+ FieldDef: def, // Save
original FieldDef for skipping
+ WriteType: writeType,
+ HasGenerics:
isCollectionType(fieldTypeId), // Container fields have declared element types
+ },
+ }
+ fields = append(fields, fieldInfo)
+ }
+ s.fields = fields
+ s.fieldGroup = GroupFields(s.fields)
+ s.typeDefDiffers = true // Unknown type, must use ordered
reading
+ return nil
+ }
+
+ // Build maps from field names and tag IDs to struct field indices
+ fieldNameToIndex := make(map[string]int)
+ fieldNameToOffset := make(map[string]uintptr)
+ fieldNameToType := make(map[string]reflect.Type)
+ localNullableByIndex := make(map[int]bool)
+ fieldTagIDToIndex := make(map[int]int) // tag ID -> struct
field index
+ fieldTagIDToOffset := make(map[int]uintptr) // tag ID -> field offset
+ fieldTagIDToType := make(map[int]reflect.Type) // tag ID -> field type
+ fieldTagIDToName := make(map[int]string) // tag ID -> snake_case
field name
+ for i := 0; i < type_.NumField(); i++ {
+ field := type_.Field(i)
+
+ // Parse fory tag and skip ignored fields
+ foryTag := parseForyTag(field)
+ if foryTag.Ignore {
+ continue
+ }
+
+ name := SnakeCase(field.Name)
+ fieldNameToIndex[name] = i
+ fieldNameToOffset[name] = field.Offset
+ fieldNameToType[name] = field.Type
+ localNullableByIndex[i] = computeLocalNullable(typeResolver,
field, foryTag)
+
+ // Also index by tag ID if present
+ if foryTag.ID >= 0 {
+ fieldTagIDToIndex[foryTag.ID] = i
+ fieldTagIDToOffset[foryTag.ID] = field.Offset
+ fieldTagIDToType[foryTag.ID] = field.Type
+ fieldTagIDToName[foryTag.ID] = name
+ }
+ }
+
+ var fields []FieldInfo
+
+ for _, def := range s.fieldDefs {
+ fieldSerializer, err :=
getFieldTypeSerializerWithResolver(typeResolver, def.fieldType)
+ if err != nil || fieldSerializer == nil {
+ // If we can't get serializer from typeID, try to get
it from the Go type
+ // This can happen when the type isn't registered in
typeIDToTypeInfo
+ remoteTypeInfo, _ :=
def.fieldType.getTypeInfoWithResolver(typeResolver)
+ if remoteTypeInfo.Type != nil {
+ fieldSerializer, _ =
typeResolver.getSerializerByType(remoteTypeInfo.Type, true)
+ }
+ }
+
+ // Get the remote type from fieldDef
+ remoteTypeInfo, _ :=
def.fieldType.getTypeInfoWithResolver(typeResolver)
+ remoteType := remoteTypeInfo.Type
+ // Track if type lookup failed - we'll need to skip such fields
+ // Note: DynamicFieldType.getTypeInfoWithResolver returns any
(not nil) when lookup fails
+ emptyInterfaceType := reflect.TypeOf((*any)(nil)).Elem()
+ typeLookupFailed := remoteType == nil || remoteType ==
emptyInterfaceType
+ if remoteType == nil {
+ remoteType = emptyInterfaceType
+ }
+
+ // For struct-like fields, even if TypeDef lookup fails, we can
try to read
+ // the field because type resolution happens at read time from
the buffer.
+ // The type name might map to a different local type.
+ isStructLikeField := isStructFieldType(def.fieldType)
+
+ // Try to find corresponding local field
+ // First try to match by tag ID (if remote def uses tag ID)
+ // Then fall back to matching by field name
+ fieldIndex := -1
+ var offset uintptr
+ var fieldType reflect.Type
+ var localFieldName string
+ var localType reflect.Type
+ var exists bool
+
+ if def.tagID >= 0 {
+ // Try to match by tag ID
+ if idx, ok := fieldTagIDToIndex[def.tagID]; ok {
+ exists = true
+ fieldIndex = idx // Will be overwritten if
types are compatible
+ localType = fieldTagIDToType[def.tagID]
+ offset = fieldTagIDToOffset[def.tagID]
+ localFieldName = fieldTagIDToName[def.tagID]
+ }
+ }
+
+ // Fall back to name-based matching if tag ID match failed
+ if !exists && def.name != "" {
+ if _, ok := fieldNameToIndex[def.name]; ok {
+ exists = true
+ localType = fieldNameToType[def.name]
+ offset = fieldNameToOffset[def.name]
+ localFieldName = def.name
+ }
+ }
+
+ if exists {
+ idx := fieldNameToIndex[localFieldName]
+ if def.tagID >= 0 {
+ idx = fieldTagIDToIndex[def.tagID]
+ }
+ // Check if types are compatible
+ // For primitive types: skip if types don't match
+ // For struct-like types: allow read even if TypeDef
lookup failed,
+ // because runtime type resolution by name might work
+ shouldRead := false
+ isPolymorphicField := def.fieldType.TypeId() == UNKNOWN
+ defTypeId := def.fieldType.TypeId()
+ // Check if field is an enum - either by type ID or by
serializer type
+ internalDefTypeId := defTypeId
+ isEnumField := internalDefTypeId == ENUM
+ if !isEnumField && fieldSerializer != nil {
+ _, isEnumField =
fieldSerializer.(*enumSerializer)
+ }
+ if isPolymorphicField && localType.Kind() ==
reflect.Interface {
+ // For polymorphic (UNKNOWN) fields with any
local type,
+ // allow reading - the actual type will be
determined at runtime
+ shouldRead = true
+ fieldType = localType
+ } else if typeLookupFailed && isEnumField {
+ // For enum fields with failed TypeDef lookup,
check if local field is a numeric type
+ // (Go enums are int-based)
+ // Also handle pointer enum fields (*EnumType)
+ localKind := localType.Kind()
+ elemKind := localKind
+ if localKind == reflect.Ptr {
+ elemKind = localType.Elem().Kind()
+ }
+ if isNumericKind(elemKind) {
+ shouldRead = true
+ fieldType = localType
+ // Get the serializer for the base type
(the enum type, not the pointer)
+ baseType := localType
+ if localKind == reflect.Ptr {
+ baseType = localType.Elem()
+ }
+ fieldSerializer, _ =
typeResolver.getSerializerByType(baseType, true)
+ }
+ } else if typeLookupFailed && isStructLikeField {
+ // For struct fields with failed TypeDef
lookup, check if local field can hold a struct
+ localKind := localType.Kind()
+ if localKind == reflect.Ptr {
+ localKind = localType.Elem().Kind()
+ }
+ if localKind == reflect.Struct || localKind ==
reflect.Interface {
+ shouldRead = true
+ fieldType = localType // Use local type
for struct fields
+ }
+ } else if typeLookupFailed && (internalDefTypeId ==
UNION || internalDefTypeId == TYPED_UNION || internalDefTypeId == NAMED_UNION) {
+ // For union fields with failed type lookup
(named unions aren't in typeIDToTypeInfo),
+ // allow reading if the local type is a union.
+ if isUnionType(localType) {
+ shouldRead = true
+ fieldType = localType
+ }
+ } else if typeLookupFailed &&
isPrimitiveType(TypeId(internalDefTypeId)) {
+ baseLocal := localType
+ if optInfo, ok := getOptionalInfo(baseLocal);
ok {
+ baseLocal = optInfo.valueType
+ }
+ if baseLocal.Kind() == reflect.Ptr {
+ baseLocal = baseLocal.Elem()
+ }
+ if
primitiveTypeIdMatchesKind(internalDefTypeId, baseLocal.Kind()) {
+ shouldRead = true
+ fieldType = localType
+ }
+ } else if typeLookupFailed &&
isPrimitiveArrayType(TypeId(internalDefTypeId)) {
+ // Primitive arrays/slices use array type IDs
but may not be registered in typeIDToTypeInfo.
+ // Allow reading using the local slice/array
type when the type IDs match.
+ localTypeId := typeIdFromKind(localType)
+ if TypeId(localTypeId&0xFF) ==
internalDefTypeId {
+ shouldRead = true
+ fieldType = localType
+ }
+ } else if typeLookupFailed && defTypeId == LIST {
+ // For list fields with failed type lookup
(e.g., named struct element types),
+ // allow reading using the local slice type.
+ if localType.Kind() == reflect.Slice {
+ elemKind := localType.Elem().Kind()
+ if elemKind == reflect.Interface ||
+ elemKind == reflect.Struct ||
+ (elemKind == reflect.Ptr &&
localType.Elem().Elem().Kind() == reflect.Struct) {
+ shouldRead = true
+ fieldType = localType
+ }
+ }
+ } else if typeLookupFailed && defTypeId == MAP {
+ // For map fields with failed type lookup
(e.g., named struct key/value types),
+ // allow reading using the local map type.
+ if localType.Kind() == reflect.Map {
+ keyKind := localType.Key().Kind()
+ valueKind := localType.Elem().Kind()
+ if keyKind == reflect.Interface ||
+ keyKind == reflect.Struct ||
+ (keyKind == reflect.Ptr &&
localType.Key().Elem().Kind() == reflect.Struct) ||
+ valueKind == reflect.Interface
||
+ valueKind == reflect.Struct ||
+ (valueKind == reflect.Ptr &&
localType.Elem().Elem().Kind() == reflect.Struct) {
+ shouldRead = true
+ fieldType = localType
+ }
+ }
+ } else if typeLookupFailed && defTypeId == SET {
+ if isSetReflectType(localType) {
+ shouldRead = true
+ fieldType = localType
+ }
+ } else if defTypeId == SET &&
isSetReflectType(localType) {
+ // Both remote and local are Set types, allow
reading
+ shouldRead = true
+ fieldType = localType
+ } else if !typeLookupFailed &&
typesCompatible(localType, remoteType) {
+ shouldRead = true
+ fieldType = localType
+ }
+
+ if shouldRead {
+ fieldIndex = idx
+ // offset was already set above when matching
by tag ID or field name
+ // For struct-like fields with failed type
lookup, get the serializer for the local type
+ if typeLookupFailed && isStructLikeField &&
fieldSerializer == nil {
+ fieldSerializer, _ =
typeResolver.getSerializerByType(localType, true)
+ }
+ // For collection fields with interface element
types, use sliceDynSerializer
+ if typeLookupFailed && (defTypeId == LIST ||
defTypeId == SET) && fieldSerializer == nil {
+ if localType.Kind() == reflect.Slice &&
localType.Elem().Kind() == reflect.Interface {
+ fieldSerializer =
mustNewSliceDynSerializer(localType.Elem())
+ }
+ }
+ // If serializer is still nil, fall back to
local type serializer.
+ if fieldSerializer == nil {
+ fieldSerializer, _ =
typeResolver.getSerializerByType(localType, true)
+ }
+ // For Set fields (fory.Set[T] =
map[T]struct{}), get the setSerializer
+ if defTypeId == SET &&
isSetReflectType(localType) && fieldSerializer == nil {
+ fieldSerializer, _ =
typeResolver.getSerializerByType(localType, true)
+ }
+ // If local type is *T and remote type is T, we
need the serializer for *T
+ // This handles Java's Integer/Long (nullable
boxed types) mapping to Go's *int32/*int64
+ if localType.Kind() == reflect.Ptr &&
localType.Elem() == remoteType {
+ fieldSerializer, _ =
typeResolver.getSerializerByType(localType, true)
+ }
+ // For pointer enum fields (*EnumType), get the
serializer for the base enum type
+ // The struct read/write code will handle
pointer dereferencing
+ if isEnumField && localType.Kind() ==
reflect.Ptr {
+ baseType := localType.Elem()
+ fieldSerializer, _ =
typeResolver.getSerializerByType(baseType, true)
+ if DebugOutputEnabled {
+ fmt.Printf("[fory-debug]
pointer enum field %s: localType=%v baseType=%v serializer=%T\n",
+ def.name, localType,
baseType, fieldSerializer)
+ }
+ }
+ // For array fields, use array serializers (not
slice serializers) even if typeID maps to slice serializer
+ // The typeID (INT16_ARRAY, etc.) is shared
between arrays and slices, but we need the correct
+ // serializer based on the actual Go type
+ if localType.Kind() == reflect.Array {
+ elemType := localType.Elem()
+ switch elemType.Kind() {
+ case reflect.Bool:
+ fieldSerializer =
boolArraySerializer{arrayType: localType}
+ case reflect.Int8:
+ fieldSerializer =
int8ArraySerializer{arrayType: localType}
+ case reflect.Int16:
+ fieldSerializer =
int16ArraySerializer{arrayType: localType}
+ case reflect.Int32:
+ fieldSerializer =
int32ArraySerializer{arrayType: localType}
+ case reflect.Int64:
+ fieldSerializer =
int64ArraySerializer{arrayType: localType}
+ case reflect.Uint8:
+ fieldSerializer =
uint8ArraySerializer{arrayType: localType}
+ case reflect.Float32:
+ fieldSerializer =
float32ArraySerializer{arrayType: localType}
+ case reflect.Float64:
+ fieldSerializer =
float64ArraySerializer{arrayType: localType}
+ case reflect.Int:
+ if
reflect.TypeOf(int(0)).Size() == 8 {
+ fieldSerializer =
int64ArraySerializer{arrayType: localType}
+ } else {
+ fieldSerializer =
int32ArraySerializer{arrayType: localType}
+ }
+ }
+ }
+ } else {
+ // Types are incompatible or unknown - use
remote type but mark field as not settable
+ fieldType = remoteType
+ fieldIndex = -1
+ offset = 0 // Don't set offset for incompatible
fields
+ }
+ } else {
+ // Field doesn't exist locally, use type from fieldDef
+ fieldType = remoteType
+ }
+
+ optionalInfo, isOptional := getOptionalInfo(fieldType)
+ baseType := fieldType
+ if isOptional {
+ if err :=
validateOptionalValueType(optionalInfo.valueType); err != nil {
+ return fmt.Errorf("field %s: %w", def.name, err)
+ }
+ baseType = optionalInfo.valueType
+ }
+ fieldKind := FieldKindValue
+ if isOptional {
+ fieldKind = FieldKindOptional
+ } else if fieldType.Kind() == reflect.Ptr {
+ fieldKind = FieldKindPointer
+ }
+ if fieldKind == FieldKindOptional {
+ // Use the Optional serializer for local Optional[T]
fields.
+ // The serializer resolved from remote type IDs is for
the element type.
+ fieldSerializer, _ =
typeResolver.getSerializerByType(fieldType, true)
+ }
+
+ // Get TypeId from FieldType's TypeId method
+ fieldTypeId := def.fieldType.TypeId()
+ // Pre-compute RefMode based on FieldDef flags (trackingRef and
nullable)
+ refMode := RefModeNone
+ if def.trackingRef {
+ refMode = RefModeTracking
+ } else if def.nullable {
+ refMode = RefModeNullOnly
+ }
+ // Pre-compute WriteType: true for struct fields in compatible
mode
+ writeType := typeResolver.Compatible() &&
isStructField(baseType)
+
+ // Pre-compute DispatchId, with special handling for
pointer-to-numeric and enum fields
+ // IMPORTANT: For compatible mode reading, we must use the
REMOTE nullable flag
+ // to determine DispatchId, because Java wrote data with its
nullable semantics.
+ var dispatchId DispatchId
+ localKind := fieldType.Kind()
+ baseKind := localKind
+ if isOptional {
+ baseKind = baseType.Kind()
+ }
+ localIsPtr := localKind == reflect.Ptr
+ localIsPrimitive := isPrimitiveDispatchKind(baseKind) ||
(localIsPtr && isPrimitiveDispatchKind(fieldType.Elem().Kind()))
+
+ if localIsPrimitive {
+ if def.nullable {
+ // Remote is nullable - use nullable DispatchId
+ dispatchId =
getDispatchIdFromTypeId(fieldTypeId, true)
+ } else {
+ // Remote is NOT nullable - use primitive
DispatchId
+ dispatchId =
getDispatchIdFromTypeId(fieldTypeId, false)
+ if dispatchId == UnknownDispatchId {
+ dispatchType := baseType
+ if dispatchType.Kind() == reflect.Ptr {
+ dispatchType =
dispatchType.Elem()
+ }
+ dispatchId = GetDispatchId(dispatchType)
+ }
+ }
+ } else {
+ dispatchType := baseType
+ if dispatchType.Kind() == reflect.Ptr {
+ dispatchType = dispatchType.Elem()
+ }
+ dispatchId = GetDispatchId(dispatchType)
+ }
+ if fieldSerializer != nil {
+ if _, ok := fieldSerializer.(*enumSerializer); ok {
+ dispatchId = EnumDispatchId
+ } else if ptrSer, ok :=
fieldSerializer.(*ptrToValueSerializer); ok {
+ if _, ok :=
ptrSer.valueSerializer.(*enumSerializer); ok {
+ dispatchId = EnumDispatchId
+ }
+ }
+ }
+
+ // Determine field name: use local field name if matched,
otherwise use def.name
+ fieldName := def.name
+ if localFieldName != "" {
+ fieldName = localFieldName
+ }
+
+ fieldInfo := FieldInfo{
+ Offset: offset,
+ DispatchId: dispatchId,
+ RefMode: refMode,
+ Kind: fieldKind,
+ Serializer: fieldSerializer,
+ Meta: &FieldMeta{
+ Name: fieldName,
+ Type: fieldType,
+ TypeId: fieldTypeId,
+ Nullable: def.nullable, // Use remote
nullable flag
+ FieldIndex: fieldIndex,
+ FieldDef: def, // Save original FieldDef
for skipping
+ WriteType: writeType,
+ HasGenerics: isCollectionType(fieldTypeId), //
Container fields have declared element types
+ OptionalInfo: optionalInfo,
+ TagID: def.tagID,
+ HasForyTag: def.tagID >= 0,
+ },
+ }
+ fields = append(fields, fieldInfo)
+ }
+
+ s.fields = fields
+ s.fieldGroup = GroupFields(s.fields)
+
+ // Debug output for field order comparison with Java
MetaSharedSerializer
+ if DebugOutputEnabled && s.type_ != nil {
+ fmt.Printf("[Go] Remote TypeDef order (%d fields):\n",
len(s.fieldDefs))
+ for i, def := range s.fieldDefs {
+ fmt.Printf("[Go] [%d] %s -> typeId=%d,
nullable=%v\n", i, def.name, def.fieldType.TypeId(), def.nullable)
+ }
+ s.fieldGroup.DebugPrint(s.type_.Name())
+ }
+
+ // Compute typeDefDiffers: true if any field doesn't exist locally, has
type mismatch,
+ // or has nullable mismatch (which affects field ordering)
+ // When typeDefDiffers is false, we can use grouped reading for better
performance
+ s.typeDefDiffers = false
+ for i, field := range fields {
+ if field.Meta.FieldIndex < 0 {
+ // Field exists in remote TypeDef but not locally
+ if DebugOutputEnabled && s.type_ != nil {
+ fmt.Printf("[Go][fory-debug] [%s]
typeDefDiffers: missing local field for remote def idx=%d name=%q tagID=%d
typeId=%d\n",
+ s.name, i, s.fieldDefs[i].name,
s.fieldDefs[i].tagID, s.fieldDefs[i].fieldType.TypeId())
+ }
+ s.typeDefDiffers = true
+ break
+ }
+ // Check if nullable flag differs between remote and local
+ // Remote nullable is stored in fieldDefs[i].nullable
+ // Local nullable is determined by whether the Go field is a
pointer type
+ if i < len(s.fieldDefs) && field.Meta.FieldIndex >= 0 {
+ remoteNullable := s.fieldDefs[i].nullable
+ // Check if local Go field is nullable based on local
field definitions
+ localNullable :=
localNullableByIndex[field.Meta.FieldIndex]
+ if remoteNullable != localNullable {
+ if DebugOutputEnabled && s.type_ != nil {
+ fmt.Printf("[Go][fory-debug] [%s]
typeDefDiffers: nullable mismatch idx=%d name=%q tagID=%d remote=%v local=%v\n",
+ s.name, i, s.fieldDefs[i].name,
s.fieldDefs[i].tagID, remoteNullable, localNullable)
+ }
+ s.typeDefDiffers = true
+ break
+ }
+ remoteTypeId :=
TypeId(s.fieldDefs[i].fieldType.TypeId())
+ localTypeId :=
typeResolver.getTypeIdByType(field.Meta.Type)
+ if localTypeId == 0 {
+ localTypeId = typeIdFromKind(field.Meta.Type)
+ }
+ localTypeId = TypeId(localTypeId)
+ if !typeIdEqualForDiff(remoteTypeId, localTypeId) {
+ if DebugOutputEnabled && s.type_ != nil {
+ fmt.Printf("[Go][fory-debug] [%s]
typeDefDiffers: type ID mismatch idx=%d name=%q tagID=%d remote=%d local=%d\n",
+ s.name, i, s.fieldDefs[i].name,
s.fieldDefs[i].tagID, remoteTypeId, localTypeId)
+ }
+ s.typeDefDiffers = true
+ break
+ }
+ }
+ }
+
+ if DebugOutputEnabled && s.type_ != nil {
+ fmt.Printf("[Go] typeDefDiffers=%v for %s\n", s.typeDefDiffers,
s.type_.Name())
+ }
+
+ return nil
+}
+
+func typeIdEqualForDiff(remoteTypeId TypeId, localTypeId TypeId) bool {
+ if remoteTypeId == localTypeId {
+ return true
+ }
+ if remoteTypeId == UNION && (localTypeId == TYPED_UNION || localTypeId
== NAMED_UNION) {
+ return true
+ }
+ if localTypeId == UNION && (remoteTypeId == TYPED_UNION || remoteTypeId
== NAMED_UNION) {
+ return true
+ }
+ // Treat byte array encodings as compatible for diffing.
+ if (remoteTypeId == INT8_ARRAY || remoteTypeId == UINT8_ARRAY ||
remoteTypeId == BINARY) &&
+ (localTypeId == INT8_ARRAY || localTypeId == UINT8_ARRAY ||
localTypeId == BINARY) {
+ return true
+ }
+ return false
+}
+
+func (s *structSerializer) computeHash() int32 {
+ // Build FieldFingerprintInfo for each field
+ fields := make([]FieldFingerprintInfo, 0, len(s.fields))
+ for _, field := range s.fields {
+ var typeId TypeId
+ isEnumField := false
+ if field.Serializer == nil {
+ typeId = UNKNOWN
+ } else {
+ typeId = field.Meta.TypeId
+ // Check if this is an enum serializer (directly or
wrapped in ptrToValueSerializer)
+ if _, ok := field.Serializer.(*enumSerializer); ok {
+ isEnumField = true
+ typeId = UNKNOWN
+ } else if ptrSer, ok :=
field.Serializer.(*ptrToValueSerializer); ok {
+ if _, ok :=
ptrSer.valueSerializer.(*enumSerializer); ok {
+ isEnumField = true
+ typeId = UNKNOWN
+ }
+ }
+ // Unions use UNION type ID in fingerprints, regardless
of typed/named variants.
+ if typeId == TYPED_UNION || typeId == NAMED_UNION ||
typeId == UNION {
+ typeId = UNION
+ }
+ // For user-defined types (struct, ext types), use
UNKNOWN in fingerprint
+ // This matches Java's behavior where user-defined
types return UNKNOWN
+ // to ensure consistent fingerprint computation across
languages
+ if isUserDefinedType(typeId) {
+ typeId = UNKNOWN
+ }
+ fieldTypeForHash := field.Meta.Type
+ if field.Kind == FieldKindOptional {
+ fieldTypeForHash =
field.Meta.OptionalInfo.valueType
+ }
+ // For fixed-size arrays with primitive elements, use
primitive array type IDs
+ if fieldTypeForHash.Kind() == reflect.Array {
+ elemKind := fieldTypeForHash.Elem().Kind()
+ switch elemKind {
+ case reflect.Int8:
+ typeId = INT8_ARRAY
+ case reflect.Uint8:
+ typeId = UINT8_ARRAY
+ case reflect.Int16:
+ typeId = INT16_ARRAY
+ case reflect.Uint16:
+ typeId = UINT16_ARRAY
+ case reflect.Int32:
+ typeId = INT32_ARRAY
+ case reflect.Uint32:
+ typeId = UINT32_ARRAY
+ case reflect.Int64:
+ typeId = INT64_ARRAY
+ case reflect.Uint64, reflect.Uint:
+ typeId = UINT64_ARRAY
+ case reflect.Float32:
+ typeId = FLOAT32_ARRAY
+ case reflect.Float64:
+ typeId = FLOAT64_ARRAY
+ default:
+ typeId = LIST
+ }
+ } else if fieldTypeForHash.Kind() == reflect.Slice {
+ if !isPrimitiveArrayType(TypeId(typeId)) &&
typeId != BINARY {
+ typeId = LIST
+ }
+ } else if fieldTypeForHash.Kind() == reflect.Map {
+ // fory.Set[T] is defined as map[T]struct{} -
check for struct{} elem type
+ if isSetReflectType(fieldTypeForHash) {
+ typeId = SET
+ } else {
+ typeId = MAP
+ }
+ }
+ }
+
+ // Determine nullable flag for xlang compatibility:
+ // - Default: false for ALL fields (xlang default - aligned
with all languages)
+ // - Primitives are always non-nullable
+ // - Can be overridden by explicit fory tag
+ nullable := field.Kind == FieldKindOptional // Optional fields
are nullable by default
+ if field.Meta.TagNullableSet {
+ // Use explicit tag value if set
+ nullable = field.Meta.TagNullable
+ }
+ // Primitives are never nullable, regardless of tag
+ fieldTypeForNullable := field.Meta.Type
+ if field.Kind == FieldKindOptional {
+ fieldTypeForNullable = field.Meta.OptionalInfo.valueType
+ }
+ if field.Kind != FieldKindOptional &&
isNonNullablePrimitiveKind(fieldTypeForNullable.Kind()) && !isEnumField {
+ nullable = false
+ }
+
+ fields = append(fields, FieldFingerprintInfo{
+ FieldID: field.Meta.TagID,
+ FieldName: SnakeCase(field.Meta.Name),
+ TypeID: typeId,
+ // Ref is based on explicit tag annotation only, NOT
runtime ref_tracking config
+ // This allows fingerprint to be computed at compile
time for C++/Rust
+ Ref: field.Meta.TagRefSet && field.Meta.TagRef,
+ Nullable: nullable,
+ })
+ }
+
+ hashString := ComputeStructFingerprint(fields)
+ data := []byte(hashString)
+ h1, _ := Murmur3Sum128WithSeed(data, 47)
+ hash := int32(h1 & 0xFFFFFFFF)
+
+ if DebugOutputEnabled {
+ fmt.Printf("[Go][fory-debug] struct %v version
fingerprint=\"%s\" version hash=%d\n", s.type_, hashString, hash)
+ }
+
+ if hash == 0 {
+ panic(fmt.Errorf("hash for type %v is 0", s.type_))
+ }
+ return hash
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]