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 f710352b6 feat(java): enhance ForyField annotation with tag ID support
for optimized serialization (#3021)
f710352b6 is described below
commit f710352b6f45d82fca1eba69ef25cb9488570f9d
Author: Mikhail Chernyakov <[email protected]>
AuthorDate: Sat Dec 20 08:35:52 2025 +0100
feat(java): enhance ForyField annotation with tag ID support for optimized
serialization (#3021)
https://github.com/apache/fory/issues/3000
Create/update a @ForyField annotation to provide extra metadata for
performance and space optimization during java/xlang serialization.
## Why?
## What does this PR do?
## Related issues
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- [x] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.../java/org/apache/fory/annotation/ForyField.java | 28 +-
.../fory/builder/BaseObjectCodecBuilder.java | 47 +-
.../main/java/org/apache/fory/meta/ClassDef.java | 123 ++-
.../java/org/apache/fory/meta/ClassDefDecoder.java | 26 +-
.../java/org/apache/fory/meta/ClassDefEncoder.java | 45 +-
.../java/org/apache/fory/meta/TypeDefDecoder.java | 19 +-
.../java/org/apache/fory/meta/TypeDefEncoder.java | 34 +-
.../org/apache/fory/resolver/ClassResolver.java | 38 +-
.../fory/serializer/AbstractObjectSerializer.java | 7 +-
.../main/java/org/apache/fory/type/Descriptor.java | 58 +-
.../apache/fory/annotation/ForyAnnotationTest.java | 48 +-
.../annotation/ForyFieldSerializationTest.java | 838 +++++++++++++++++++++
.../apache/fory/annotation/ForyFieldTagIdTest.java | 177 +++++
.../org/apache/fory/annotation/ForyFieldTest.java | 264 +++++++
.../org/apache/fory/meta/ClassDefEncoderTest.java | 72 +-
.../java/org/apache/fory/meta/ClassDefTest.java | 307 ++++++++
.../org/apache/fory/meta/TypeDefEncoderTest.java | 369 +++++++++
.../apache/fory/type/DescriptorGrouperTest.java | 36 +-
.../java/org/apache/fory/type/DescriptorTest.java | 2 +-
.../format/encoder/ImplementInterfaceTest.java | 2 +-
20 files changed, 2404 insertions(+), 136 deletions(-)
diff --git
a/java/fory-core/src/main/java/org/apache/fory/annotation/ForyField.java
b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyField.java
index 6c42e54ac..6b83f35e4 100644
--- a/java/fory-core/src/main/java/org/apache/fory/annotation/ForyField.java
+++ b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyField.java
@@ -28,9 +28,31 @@ import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface ForyField {
- /** Whether field is nullable, default false. */
+ /**
+ * Field tag ID for schema evolution mode (REQUIRED).
+ *
+ * <ul>
+ * <li>When >= 0: Uses this numeric ID instead of field name string for
compact encoding
+ * <li>When -1: Explicitly opt-out of tag ID, use field name with meta
string encoding
+ * </ul>
+ *
+ * <p>Must be unique within the class (except -1) and stable across versions.
+ */
+ int id();
+
+ /**
+ * Whether this field can be null. When set to false (default), Fory skips
writing the null flag
+ * (saves 1 byte). When set to true, Fory writes null flag for nullable
fields. Default: false
+ * (field is non-nullable, aligned with xlang protocol defaults)
+ */
boolean nullable() default false;
- /** Whether field need trackingRef, default false. */
- boolean trackingRef() default false;
+ /**
+ * Whether to track references for this field. When set to false (default):
- Avoids adding the
+ * object to IdentityMap (saves hash map overhead) - Skips writing ref
tracking flag (saves 1 byte
+ * when combined with nullable=false) When set to true, enables reference
tracking for
+ * shared/circular references. Default: false (no reference tracking,
aligned with xlang protocol
+ * defaults)
+ */
+ boolean ref() default false;
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
index 337cfe0a7..e1be80dc8 100644
---
a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
+++
b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
@@ -405,7 +405,14 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
TypeRef<?> typeRef = descriptor.getTypeRef();
boolean nullable = descriptor.isNullable();
+ boolean useRefTracking;
if (needWriteRef(typeRef)) {
+ useRefTracking = descriptor.isTrackingRef();
+ } else {
+ useRefTracking = false;
+ }
+
+ if (useRefTracking) {
return new If(
not(writeRefOrNull(buffer, fieldValue)),
serializeForNotNullForField(fieldValue, buffer, typeRef, null,
false));
@@ -1768,20 +1775,46 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
TypeRef<?> typeRef = descriptor.getTypeRef();
boolean nullable = descriptor.isNullable();
+ boolean typeNeedsRef = needWriteRef(typeRef);
+ boolean useRefTracking;
if (needWriteRef(typeRef)) {
+ useRefTracking = descriptor.isTrackingRef();
+ } else {
+ useRefTracking = false;
+ }
+
+ if (useRefTracking) {
return readRef(buffer, callback, () ->
deserializeForNotNullForField(buffer, typeRef, null));
} else {
if (!nullable) {
Expression value = deserializeForNotNullForField(buffer, typeRef,
null);
- // Should put value expr ahead to avoid generated code in wrong scope.
+
+ if (typeNeedsRef) {
+ // When a field explicitly disables ref tracking
(@ForyField(trackingRef=false))
+ // but the type normally needs ref tracking (e.g., collections),
+ // we need to preserve a -1 id so that when the deserializer calls
reference(),
+ // it will pop this -1 and skip the setReadObject call.
+ Expression preserveStubRefId =
+ new Invoke(refResolverRef, "preserveRefId", new Literal(-1,
PRIMITIVE_INT_TYPE));
+ return new ListExpression(preserveStubRefId, value,
callback.apply(value));
+ }
return new ListExpression(value, callback.apply(value));
}
- return readNullable(
- buffer,
- typeRef,
- callback,
- () -> deserializeForNotNullForField(buffer, typeRef, null),
- true);
+
+ Expression readNullableExpr =
+ readNullable(
+ buffer,
+ typeRef,
+ callback,
+ () -> deserializeForNotNullForField(buffer, typeRef, null),
+ true);
+
+ if (typeNeedsRef) {
+ Expression preserveStubRefId =
+ new Invoke(refResolverRef, "preserveRefId", new Literal(-1,
PRIMITIVE_INT_TYPE));
+ return new ListExpression(preserveStubRefId, readNullableExpr);
+ }
+ return readNullableExpr;
}
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java
b/java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java
index d457be7a5..4855a41a6 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java
@@ -40,8 +40,10 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
+import java.util.TreeMap;
import java.util.stream.Collectors;
import org.apache.fory.Fory;
+import org.apache.fory.annotation.ForyField;
import org.apache.fory.builder.MetaSharedCodecBuilder;
import org.apache.fory.collection.Tuple2;
import org.apache.fory.config.CompatibleMode;
@@ -61,6 +63,7 @@ import org.apache.fory.serializer.NonexistentClass;
import org.apache.fory.serializer.converter.FieldConverter;
import org.apache.fory.serializer.converter.FieldConverters;
import org.apache.fory.type.Descriptor;
+import org.apache.fory.type.DescriptorBuilder;
import org.apache.fory.type.FinalObjectTypeStub;
import org.apache.fory.type.GenericType;
import org.apache.fory.type.TypeUtils;
@@ -349,17 +352,76 @@ public class ClassDef implements Serializable {
SortedMap<Member, Descriptor> allDescriptorsMap =
resolver.getFory().getClassResolver().getAllDescriptorsMap(cls,
true);
Map<String, Descriptor> descriptorsMap = new HashMap<>();
+ Map<Short, Descriptor> fieldIdToDescriptorMap = new HashMap<>();
+ Map<Member, Descriptor>[] newDescriptors = new Map[] {null};
+
for (Map.Entry<Member, Descriptor> e : allDescriptorsMap.entrySet()) {
- if (descriptorsMap.put(
- e.getKey().getDeclaringClass().getName() + "." +
e.getKey().getName(), e.getValue())
- != null) {
+ String fullName = e.getKey().getDeclaringClass().getName() + "." +
e.getKey().getName();
+ Descriptor desc = e.getValue();
+ if (descriptorsMap.put(fullName, desc) != null) {
throw new IllegalStateException("Duplicate key");
}
+
+ if (e.getKey() instanceof Field) {
+ boolean refTracking = resolver.getFory().trackingRef();
+ ForyField foryField = desc.getForyField();
+ // update ref tracking if
+ // - global ref tracking is disabled but field is tracking ref
(@ForyField#ref set)
+ // - global ref tracking is enabled but field is not tracking ref
(@ForyField#ref not set)
+ boolean needsUpdate =
+ (refTracking && foryField == null && !desc.isTrackingRef())
+ || (foryField != null && desc.isTrackingRef());
+
+ if (needsUpdate) {
+ if (newDescriptors[0] == null) {
+ newDescriptors[0] = new HashMap<>();
+ }
+ boolean newTrackingRef = refTracking && foryField == null;
+ Descriptor newDescriptor =
+ new
DescriptorBuilder(desc).trackingRef(newTrackingRef).build();
+
+ descriptorsMap.put(fullName, newDescriptor);
+ desc = newDescriptor;
+ newDescriptors[0].put(e.getKey(), newDescriptor);
+ }
+ }
+
+ // If the field has @ForyField annotation with field ID, index by
field ID
+ if (desc.getForyField() != null) {
+ int fieldId = desc.getForyField().id();
+ if (fieldId >= 0) {
+ if (fieldIdToDescriptorMap.containsKey((short) fieldId)) {
+ throw new IllegalArgumentException(
+ "Duplicate field id "
+ + fieldId
+ + " for field "
+ + desc.getName()
+ + " in class "
+ + cls.getName());
+ }
+ fieldIdToDescriptorMap.put((short) fieldId, desc);
+ }
+ }
}
+
+ if (newDescriptors[0] != null) {
+ SortedMap<Member, Descriptor> allDescriptorsCopy = new
TreeMap<>(allDescriptorsMap);
+ allDescriptorsCopy.putAll(newDescriptors[0]);
+ resolver.getFory().getClassResolver().updateDescriptorsCache(cls,
true, allDescriptorsCopy);
+ }
+
descriptors = new ArrayList<>(fieldsInfo.size());
for (FieldInfo fieldInfo : fieldsInfo) {
- Descriptor descriptor =
- descriptorsMap.get(fieldInfo.getDefinedClass() + "." +
fieldInfo.getFieldName());
+ Descriptor descriptor;
+
+ // Try to match by field ID first if the FieldInfo has an ID
+ if (fieldInfo.hasFieldId()) {
+ descriptor = fieldIdToDescriptorMap.get(fieldInfo.getFieldId());
+ } else {
+ descriptor =
+ descriptorsMap.get(fieldInfo.getDefinedClass() + "." +
fieldInfo.getFieldName());
+ }
+
Descriptor newDesc = fieldInfo.toDescriptor(resolver, descriptor);
Class<?> rawType = newDesc.getRawType();
FieldType fieldType = fieldInfo.getFieldType();
@@ -422,10 +484,18 @@ public class ClassDef implements Serializable {
private final FieldType fieldType;
+ /** Field ID for schema evolution, -1 means no field ID (use field name).
*/
+ private final short fieldId;
+
FieldInfo(String definedClass, String fieldName, FieldType fieldType) {
+ this(definedClass, fieldName, fieldType, (short) -1);
+ }
+
+ FieldInfo(String definedClass, String fieldName, FieldType fieldType,
short fieldId) {
this.definedClass = definedClass;
this.fieldName = fieldName;
this.fieldType = fieldType;
+ this.fieldId = fieldId;
}
/** Returns classname of current field defined. */
@@ -439,13 +509,13 @@ public class ClassDef implements Serializable {
}
/** Returns whether field is annotated by an unsigned int id. */
- public boolean hasTag() {
- return false;
+ public boolean hasFieldId() {
+ return fieldId >= 0;
}
- /** Returns annotated tag id for the field. */
- public short getTag() {
- return -1;
+ /** Returns annotated field-id for the field. */
+ public short getFieldId() {
+ return fieldId;
}
/** Returns type of current field. */
@@ -465,13 +535,15 @@ public class ClassDef implements Serializable {
if (typeRef.equals(declared)) {
return descriptor;
} else {
+ // TODO fix return here
descriptor.copyWithTypeName(typeRef.getType().getTypeName());
}
}
// This field doesn't exist in peer class, so any legal modifier will be
OK.
// Use constant instead of reflection to avoid GraalVM native image
issues.
int stubModifiers = Modifier.PRIVATE | Modifier.FINAL;
- return new Descriptor(typeRef, fieldName, stubModifiers, definedClass);
+ return new Descriptor(
+ typeRef, fieldName, stubModifiers, definedClass,
resolver.needToWriteRef(typeRef));
}
@Override
@@ -483,14 +555,15 @@ public class ClassDef implements Serializable {
return false;
}
FieldInfo fieldInfo = (FieldInfo) o;
- return Objects.equals(definedClass, fieldInfo.definedClass)
+ return fieldId == fieldInfo.fieldId
+ && Objects.equals(definedClass, fieldInfo.definedClass)
&& Objects.equals(fieldName, fieldInfo.fieldName)
&& Objects.equals(fieldType, fieldInfo.fieldType);
}
@Override
public int hashCode() {
- return Objects.hash(definedClass, fieldName, fieldType);
+ return Objects.hash(definedClass, fieldName, fieldType, fieldId);
}
@Override
@@ -502,6 +575,7 @@ public class ClassDef implements Serializable {
+ ", fieldName='"
+ fieldName
+ '\''
+ + (fieldId >= 0 ? ", fieldID=" + fieldId : "")
+ ", fieldType="
+ fieldType
+ '}';
@@ -1089,11 +1163,12 @@ public class ClassDef implements Serializable {
static FieldType buildFieldType(TypeResolver resolver, Field field) {
Preconditions.checkNotNull(field);
GenericType genericType =
resolver.buildGenericType(field.getGenericType());
- return buildFieldType(resolver, genericType);
+ return buildFieldType(resolver, field, genericType);
}
/** Build field type from generics, nested generics will be extracted too. */
- private static FieldType buildFieldType(TypeResolver resolver, GenericType
genericType) {
+ private static FieldType buildFieldType(
+ TypeResolver resolver, Field field, GenericType genericType) {
Preconditions.checkNotNull(genericType);
Class<?> rawType = genericType.getCls();
boolean isXlang = resolver.getFory().isCrossLanguage();
@@ -1108,8 +1183,17 @@ public class ClassDef implements Serializable {
}
boolean isMonomorphic = genericType.isMonomorphic();
boolean trackingRef = genericType.trackingRef(resolver);
- // TODO support @Nullable/ForyField annotation
boolean nullable = !genericType.getCls().isPrimitive();
+
+ // Apply @ForyField annotation if present
+ if (field != null) {
+ ForyField foryField = field.getAnnotation(ForyField.class);
+ if (foryField != null) {
+ nullable = foryField.nullable();
+ trackingRef = foryField.ref();
+ }
+ }
+
if (COLLECTION_TYPE.isSupertypeOf(genericType.getTypeRef())) {
return new CollectionFieldType(
xtypeId,
@@ -1118,6 +1202,7 @@ public class ClassDef implements Serializable {
trackingRef,
buildFieldType(
resolver,
+ null, // nested fields don't have Field reference
genericType.getTypeParameter0() == null
? GenericType.build(Object.class)
: genericType.getTypeParameter0()));
@@ -1129,11 +1214,13 @@ public class ClassDef implements Serializable {
trackingRef,
buildFieldType(
resolver,
+ null, // nested fields don't have Field reference
genericType.getTypeParameter0() == null
? GenericType.build(Object.class)
: genericType.getTypeParameter0()),
buildFieldType(
resolver,
+ null, // nested fields don't have Field reference
genericType.getTypeParameter1() == null
? GenericType.build(Object.class)
: genericType.getTypeParameter1()));
@@ -1160,7 +1247,7 @@ public class ClassDef implements Serializable {
isMonomorphic,
nullable,
trackingRef,
- buildFieldType(resolver, GenericType.build(elemType)));
+ buildFieldType(resolver, null, GenericType.build(elemType)));
}
Tuple2<Class<?>, Integer> info =
TypeUtils.getArrayComponentInfo(rawType);
return new ArrayFieldType(
@@ -1168,7 +1255,7 @@ public class ClassDef implements Serializable {
isMonomorphic,
nullable,
trackingRef,
- buildFieldType(resolver, GenericType.build(info.f0)),
+ buildFieldType(resolver, null, GenericType.build(info.f0)),
info.f1);
}
return new ObjectFieldType(xtypeId, isMonomorphic, nullable,
trackingRef);
diff --git
a/java/fory-core/src/main/java/org/apache/fory/meta/ClassDefDecoder.java
b/java/fory-core/src/main/java/org/apache/fory/meta/ClassDefDecoder.java
index 1c9b41607..e2cb372c9 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/ClassDefDecoder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/ClassDefDecoder.java
@@ -118,23 +118,37 @@ class ClassDefDecoder {
int header = buffer.readByte() & 0xff;
// `3 bits size + 2 bits field name encoding + polymorphism flag +
nullability flag + ref
// tracking flag`
- // TODO(chaokunyang) read type tag
int encodingFlags = (header >>> 3) & 0b11;
boolean useTagID = encodingFlags == 3;
- Preconditions.checkArgument(
- !useTagID, "Type tag not supported currently, parsed fieldInfos %s",
fieldInfos);
int size = header >>> 5;
if (size == 7) {
size += buffer.readVarUint32Small7();
}
size += 1;
- Encoding encoding = fieldNameEncodings[encodingFlags];
- String fieldName =
Encoders.FIELD_NAME_DECODER.decode(buffer.readBytes(size), encoding);
+
+ // Read field name or tag ID
+ String fieldName;
+ short tagId = -1;
+ if (useTagID) {
+ // When useTagID is true, size contains the tag ID
+ tagId = (short) (size - 1);
+ // Use placeholder field name since tag ID is used for identification
+ fieldName = "$tag" + tagId;
+ } else {
+ Encoding encoding = fieldNameEncodings[encodingFlags];
+ fieldName = Encoders.FIELD_NAME_DECODER.decode(buffer.readBytes(size),
encoding);
+ }
+
boolean isMonomorphic = (header & 0b100) != 0;
boolean trackingRef = (header & 0b001) != 0;
int typeId = buffer.readVarUint32Small14();
FieldType fieldType = FieldType.read(buffer, resolver, isMonomorphic,
trackingRef, typeId);
- fieldInfos.add(new ClassDef.FieldInfo(className, fieldName, fieldType));
+
+ if (useTagID) {
+ fieldInfos.add(new ClassDef.FieldInfo(className, fieldName, fieldType,
tagId));
+ } else {
+ fieldInfos.add(new ClassDef.FieldInfo(className, fieldName,
fieldType));
+ }
}
return fieldInfos;
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/meta/ClassDefEncoder.java
b/java/fory-core/src/main/java/org/apache/fory/meta/ClassDefEncoder.java
index c75d7edaf..121735c04 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/ClassDefEncoder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/ClassDefEncoder.java
@@ -30,11 +30,14 @@ import static
org.apache.fory.meta.Encoders.typeNameEncodingsList;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
import org.apache.fory.Fory;
+import org.apache.fory.annotation.ForyField;
import org.apache.fory.annotation.Internal;
import org.apache.fory.collection.Tuple2;
import org.apache.fory.memory.MemoryBuffer;
@@ -90,12 +93,38 @@ public class ClassDefEncoder {
public static List<FieldInfo> buildFieldsInfo(TypeResolver resolver,
List<Field> fields) {
List<FieldInfo> fieldInfos = new ArrayList<>();
+ Set<Integer> usedTagIds = new HashSet<>();
for (Field field : fields) {
- FieldInfo fieldInfo =
- new FieldInfo(
- field.getDeclaringClass().getName(),
- field.getName(),
- ClassDef.buildFieldType(resolver, field));
+ // Check for @ForyField annotation to extract tag ID
+ ForyField foryField = field.getAnnotation(ForyField.class);
+ FieldType fieldType = ClassDef.buildFieldType(resolver, field);
+
+ FieldInfo fieldInfo;
+ if (foryField != null) {
+ int tagId = foryField.id();
+ if (tagId >= 0) {
+ if (!usedTagIds.add(tagId)) {
+ throw new IllegalArgumentException(
+ "Duplicate tag id: "
+ + tagId
+ + ", field: "
+ + field
+ + ", class: "
+ + field.getDeclaringClass());
+ }
+ // Create FieldInfo with tag ID for optimized serialization
+ fieldInfo =
+ new FieldInfo(
+ field.getDeclaringClass().getName(), field.getName(),
fieldType, (short) tagId);
+ } else {
+ // tagId == -1 means opt-out, use field name
+ fieldInfo =
+ new FieldInfo(field.getDeclaringClass().getName(),
field.getName(), fieldType);
+ }
+ } else {
+ // No annotation, use field name
+ fieldInfo = new FieldInfo(field.getDeclaringClass().getName(),
field.getName(), fieldType);
+ }
fieldInfos.add(fieldInfo);
}
return fieldInfos;
@@ -261,8 +290,8 @@ public class ClassDefEncoder {
int encodingFlags =
fieldNameEncodingsList.indexOf(metaString.getEncoding());
byte[] encoded = metaString.getBytes();
int size = (encoded.length - 1);
- if (fieldInfo.hasTag()) {
- size = fieldInfo.getTag();
+ if (fieldInfo.hasFieldId()) {
+ size = fieldInfo.getFieldId();
encodingFlags = 3;
}
header |= (byte) (encodingFlags << 3);
@@ -275,7 +304,7 @@ public class ClassDefEncoder {
header |= (size << 5);
buffer.writeByte(header);
}
- if (!fieldInfo.hasTag()) {
+ if (!fieldInfo.hasFieldId()) {
buffer.writeBytes(encoded);
}
fieldType.write(buffer, false);
diff --git
a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefDecoder.java
b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefDecoder.java
index d67dd1030..314f82cdb 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefDecoder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefDecoder.java
@@ -118,15 +118,20 @@ class TypeDefDecoder {
boolean trackingRef = (header & 0b1) != 0;
int typeId = buffer.readVarUint32Small14();
FieldType fieldType = FieldType.xread(buffer, resolver, typeId,
nullable, trackingRef);
- // read field name
+
+ // read field name or tag ID
if (useTagID) {
- throw new UnsupportedOperationException(
- "Type tag not supported currently, parsed fieldInfos %s " +
fieldInfos);
+ // When useTagID is true, fieldNameSize actually contains the tag ID
+ short tagId = (short) (fieldNameSize - 1);
+ // Use a placeholder field name since tag ID is used for identification
+ String fieldName = "$tag" + tagId; // TODO we could use id as String
as field name
+ fieldInfos.add(new ClassDef.FieldInfo(className, fieldName, fieldType,
tagId));
+ } else {
+ Encoding encoding = fieldNameEncodings[encodingFlags];
+ String fieldName =
+
Encoders.FIELD_NAME_DECODER.decode(buffer.readBytes(fieldNameSize), encoding);
+ fieldInfos.add(new ClassDef.FieldInfo(className, fieldName,
fieldType));
}
- Encoding encoding = fieldNameEncodings[encodingFlags];
- String fieldName =
- Encoders.FIELD_NAME_DECODER.decode(buffer.readBytes(fieldNameSize),
encoding);
- fieldInfos.add(new ClassDef.FieldInfo(className, fieldName, fieldType));
}
return fieldInfos;
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefEncoder.java
b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefEncoder.java
index aabe12f8e..a39029fb1 100644
--- a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefEncoder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDefEncoder.java
@@ -26,12 +26,15 @@ import static
org.apache.fory.meta.Encoders.fieldNameEncodingsList;
import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.fory.Fory;
+import org.apache.fory.annotation.ForyField;
import org.apache.fory.logging.Logger;
import org.apache.fory.logging.LoggerFactory;
import org.apache.fory.memory.MemoryBuffer;
@@ -79,11 +82,30 @@ class TypeDefEncoder {
}
static List<FieldInfo> buildFieldsInfo(TypeResolver resolver, Class<?> type,
List<Field> fields) {
+ Set<Integer> usedTagIds = new HashSet<>();
return fields.stream()
.map(
- field ->
- new FieldInfo(
- type.getName(), field.getName(),
ClassDef.buildFieldType(resolver, field)))
+ field -> {
+ ForyField foryField = field.getAnnotation(ForyField.class);
+ FieldType fieldType = ClassDef.buildFieldType(resolver, field);
+ if (foryField != null) {
+ int tagId = foryField.id();
+ if (tagId >= 0) {
+ if (!usedTagIds.add(tagId)) {
+ throw new IllegalArgumentException(
+ "Duplicate tag id "
+ + tagId
+ + " for field "
+ + field.getName()
+ + " in class "
+ + type.getName());
+ }
+ return new FieldInfo(type.getName(), field.getName(),
fieldType, (short) tagId);
+ }
+ // tagId == -1 means use field name, fall through to create
regular FieldInfo
+ }
+ return new FieldInfo(type.getName(), field.getName(), fieldType);
+ })
.collect(Collectors.toList());
}
@@ -171,8 +193,8 @@ class TypeDefEncoder {
header |= fieldType.nullable() ? 0b10 : 0b00;
int size, encodingFlags;
byte[] encoded = null;
- if (fieldInfo.hasTag()) {
- size = fieldInfo.getTag();
+ if (fieldInfo.hasFieldId()) {
+ size = fieldInfo.getFieldId();
encodingFlags = 3;
} else {
MetaString metaString =
Encoders.encodeFieldName(fieldInfo.getFieldName());
@@ -193,7 +215,7 @@ class TypeDefEncoder {
}
fieldType.xwrite(buffer, false);
// write field name
- if (!fieldInfo.hasTag()) {
+ if (!fieldInfo.hasFieldId()) {
buffer.writeBytes(encoded);
}
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index 3b87c6611..bc2c0276d 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -146,6 +146,7 @@ import
org.apache.fory.serializer.scala.SingletonObjectSerializer;
import org.apache.fory.serializer.shim.ProtobufDispatcher;
import org.apache.fory.serializer.shim.ShimDispatcher;
import org.apache.fory.type.Descriptor;
+import org.apache.fory.type.DescriptorBuilder;
import org.apache.fory.type.DescriptorGrouper;
import org.apache.fory.type.GenericType;
import org.apache.fory.type.TypeUtils;
@@ -1216,12 +1217,42 @@ public class ClassResolver extends TypeResolver {
public List<Descriptor> getFieldDescriptors(Class<?> clz, boolean
searchParent) {
SortedMap<Member, Descriptor> allDescriptors = getAllDescriptorsMap(clz,
searchParent);
List<Descriptor> result = new ArrayList<>(allDescriptors.size());
+
+ Map<Member, Descriptor>[] newDescriptors = new Map[] {null};
+
allDescriptors.forEach(
(member, descriptor) -> {
- if (member instanceof Field) {
+ if (!(member instanceof Field)) {
+ return;
+ }
+
+ boolean shouldTrack = fory.trackingRef();
+ boolean hasForyField = descriptor.getForyField() != null;
+ // update ref tracking if
+ // - global ref tracking is enabled but field is not tracking ref
(@ForyField#ref not set)
+ // - global ref tracking is disabled but field is tracking ref
(@ForyField#ref set)
+ boolean needsUpdate =
+ (shouldTrack && !hasForyField)
+ || (!shouldTrack && hasForyField &&
descriptor.isTrackingRef());
+
+ if (needsUpdate) {
+ if (newDescriptors[0] == null) {
+ newDescriptors[0] = new HashMap<>();
+ }
+ Descriptor newDescriptor =
+ new
DescriptorBuilder(descriptor).trackingRef(shouldTrack).build();
+ result.add(newDescriptor);
+ newDescriptors[0].put(member, newDescriptor);
+ } else {
result.add(descriptor);
}
});
+
+ if (newDescriptors[0] != null) {
+ SortedMap<Member, Descriptor> allDescriptorsCopy = new
TreeMap<>(allDescriptors);
+ allDescriptorsCopy.putAll(newDescriptors[0]);
+ extRegistry.descriptorsCache.put(Tuple2.of(clz, searchParent),
allDescriptorsCopy);
+ }
return result;
}
@@ -1232,6 +1263,11 @@ public class ClassResolver extends TypeResolver {
Tuple2.of(clz, searchParent), t ->
Descriptor.getAllDescriptorsMap(clz, searchParent));
}
+ public void updateDescriptorsCache(
+ Class<?> clz, boolean searchParent, SortedMap<Member, Descriptor>
descriptors) {
+ extRegistry.descriptorsCache.put(Tuple2.of(clz, searchParent),
descriptors);
+ }
+
public ClassInfo getClassInfo(short classId) {
ClassInfo classInfo = registeredId2ClassInfo[classId];
assert classInfo != null : classId;
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java
index 7ecde0993..8ff29fcb4 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java
@@ -29,7 +29,6 @@ import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.fory.Fory;
-import org.apache.fory.annotation.ForyField;
import org.apache.fory.collection.Tuple2;
import org.apache.fory.collection.Tuple3;
import org.apache.fory.memory.MemoryBuffer;
@@ -996,13 +995,9 @@ public abstract class AbstractObjectSerializer<T> extends
Serializer<T> {
this.qualifiedFieldName = d.getDeclaringClass() + "." + d.getName();
this.fieldAccessor = d.getField() != null ?
FieldAccessor.createAccessor(d.getField()) : null;
fieldConverter = d.getFieldConverter();
- ForyField foryField = d.getForyField();
nullable = d.isNullable();
if (fory.trackingRef()) {
- trackingRef =
- foryField != null
- ? foryField.trackingRef()
- : fory.getClassResolver().needToWriteRef(typeRef);
+ trackingRef = d.isTrackingRef();
}
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/type/Descriptor.java
b/java/fory-core/src/main/java/org/apache/fory/type/Descriptor.java
index decb2b93a..a65574737 100644
--- a/java/fory-core/src/main/java/org/apache/fory/type/Descriptor.java
+++ b/java/fory-core/src/main/java/org/apache/fory/type/Descriptor.java
@@ -89,6 +89,8 @@ public class Descriptor {
private final Method writeMethod;
private ForyField foryField;
private boolean nullable;
+ // trackingRef should only be true if explicitly set to true via
@ForyField(ref=true)
+ // If no annotation or ref not specified, trackingRef stays false and
type-based tracking applies
private boolean trackingRef;
private FieldConverter<?> fieldConverter;
@@ -105,9 +107,11 @@ public class Descriptor {
if (!typeRef.isPrimitive()) {
this.nullable = foryField == null || foryField.nullable();
}
+ this.trackingRef = foryField != null && foryField.ref();
}
- public Descriptor(TypeRef<?> typeRef, String name, int modifier, String
declaringClass) {
+ public Descriptor(
+ TypeRef<?> typeRef, String name, int modifier, String declaringClass,
boolean trackingRef) {
this.field = null;
this.typeName = typeRef.getRawType().getName();
this.name = name;
@@ -118,6 +122,7 @@ public class Descriptor {
this.writeMethod = null;
this.foryField = null;
this.nullable = !typeRef.isPrimitive();
+ this.trackingRef = trackingRef;
}
private Descriptor(Field field, Method readMethod) {
@@ -133,6 +138,7 @@ public class Descriptor {
if (!field.getType().isPrimitive()) {
this.nullable = foryField == null || foryField.nullable();
}
+ this.trackingRef = foryField != null && foryField.ref();
}
private Descriptor(Method readMethod) {
@@ -148,45 +154,24 @@ public class Descriptor {
if (!readMethod.getReturnType().isPrimitive()) {
this.nullable = foryField == null || foryField.nullable();
}
+ this.trackingRef = foryField != null && foryField.ref();
}
- private Descriptor(
- TypeRef<?> typeRef,
- String typeName,
- String name,
- int modifier,
- String declaringClass,
- Field field,
- Method readMethod,
- Method writeMethod) {
- this.typeRef = typeRef;
- this.typeName = typeName;
- this.name = name;
- this.modifier = modifier;
- this.declaringClass = declaringClass;
- this.field = field;
- this.readMethod = readMethod;
- this.writeMethod = writeMethod;
+ public Descriptor(DescriptorBuilder builder) {
+ this.typeRef = builder.typeRef;
+ this.typeName = builder.typeName;
+ this.name = builder.name;
+ this.modifier = builder.modifier;
+ this.declaringClass = builder.declaringClass;
+ this.field = builder.field;
+ this.readMethod = builder.readMethod;
+ this.writeMethod = builder.writeMethod;
+ this.trackingRef = builder.trackingRef;
this.foryField = this.field == null ? null :
this.field.getAnnotation(ForyField.class);
if (!typeRef.isPrimitive()) {
this.nullable = foryField == null || foryField.nullable();
}
- }
-
- public Descriptor(DescriptorBuilder builder) {
- this(
- builder.typeRef,
- builder.typeName,
- builder.name,
- builder.modifier,
- builder.declaringClass,
- builder.field,
- builder.readMethod,
- builder.writeMethod);
- this.nullable = builder.nullable;
- this.trackingRef = builder.trackingRef;
this.type = builder.type;
- this.foryField = builder.foryField;
this.fieldConverter = builder.fieldConverter;
}
@@ -309,6 +294,9 @@ public class Descriptor {
sb.append(", typeRef=").append(typeRef);
}
sb.append(", foryField=").append(foryField);
+ sb.append(", trackingRef=").append(trackingRef);
+ sb.append(", foryFieldConverter=").append(fieldConverter);
+ sb.append(", nullable=").append(nullable);
sb.append('}');
return sb.toString();
}
@@ -322,7 +310,7 @@ public class Descriptor {
SortedMap<Member, Descriptor> allDescriptorsMap =
getAllDescriptorsMap(clz);
Map<String, List<Member>> duplicateNameFields =
getDuplicateNames(allDescriptorsMap);
checkArgument(
- duplicateNameFields.size() == 0, "%s has duplicate fields %s", clz,
duplicateNameFields);
+ duplicateNameFields.isEmpty(), "%s has duplicate fields %s", clz,
duplicateNameFields);
return new ArrayList<>(allDescriptorsMap.values());
}
@@ -334,7 +322,7 @@ public class Descriptor {
SortedMap<Member, Descriptor> allDescriptorsMap =
getAllDescriptorsMap(clz);
Map<String, List<Member>> duplicateNameFields =
getDuplicateNames(allDescriptorsMap);
Preconditions.checkArgument(
- duplicateNameFields.size() == 0, "%s has duplicate fields %s", clz,
duplicateNameFields);
+ duplicateNameFields.isEmpty(), "%s has duplicate fields %s", clz,
duplicateNameFields);
TreeMap<String, Descriptor> map = new TreeMap<>();
allDescriptorsMap.forEach((k, v) -> map.put(k.getName(), v));
return map;
diff --git
a/java/fory-core/src/test/java/org/apache/fory/annotation/ForyAnnotationTest.java
b/java/fory-core/src/test/java/org/apache/fory/annotation/ForyAnnotationTest.java
index 11b35929f..300ae1a87 100644
---
a/java/fory-core/src/test/java/org/apache/fory/annotation/ForyAnnotationTest.java
+++
b/java/fory-core/src/test/java/org/apache/fory/annotation/ForyAnnotationTest.java
@@ -35,10 +35,10 @@ public class ForyAnnotationTest extends ForyTestBase {
@Data
public static class BeanM {
- @ForyField(nullable = false)
+ @ForyField(id = 0, nullable = false)
public Long f1;
- @ForyField(nullable = false)
+ @ForyField(id = 1, nullable = false)
private Long f2;
String s = "str";
@@ -64,58 +64,61 @@ public class ForyAnnotationTest extends ForyTestBase {
byte byte1;
- @ForyField int i3 = 10;
+ @ForyField(id = 2)
+ int i3 = 10;
- @ForyField List<Integer> integerList = Lists.newArrayList(1);
+ @ForyField(id = 3)
+ List<Integer> integerList = Lists.newArrayList(1);
- @ForyField String s1 = "str";
+ @ForyField(id = 4)
+ String s1 = "str";
- @ForyField(nullable = false)
+ @ForyField(id = 5, nullable = false)
Short shortValue1 = Short.valueOf((short) 2);
- @ForyField(nullable = false)
+ @ForyField(id = 6, nullable = false)
Byte byteValue1 = Byte.valueOf((byte) 3);
- @ForyField(nullable = false)
+ @ForyField(id = 7, nullable = false)
Long longValue1 = Long.valueOf(4L);
- @ForyField(nullable = false)
+ @ForyField(id = 8, nullable = false)
Boolean booleanValue1 = Boolean.TRUE;
- @ForyField(nullable = false)
+ @ForyField(id = 9, nullable = false)
Float floatValue1 = Float.valueOf(5.0f);
- @ForyField(nullable = false)
+ @ForyField(id = 10, nullable = false)
Double doubleValue1 = Double.valueOf(6.0);
- @ForyField(nullable = false)
+ @ForyField(id = 11, nullable = false)
Character character1 = Character.valueOf('c');
- @ForyField(nullable = true)
+ @ForyField(id = 12, nullable = true)
List<Integer> integerList1 = Lists.newArrayList(1);
- @ForyField(nullable = true)
+ @ForyField(id = 13, nullable = true)
String s2 = "str";
- @ForyField(nullable = true)
+ @ForyField(id = 14, nullable = true)
Short shortValue2 = Short.valueOf((short) 2);
- @ForyField(nullable = true)
+ @ForyField(id = 15, nullable = true)
Byte byteValue2 = Byte.valueOf((byte) 3);
- @ForyField(nullable = true)
+ @ForyField(id = 16, nullable = true)
Long longValue2 = Long.valueOf(4L);
- @ForyField(nullable = true)
+ @ForyField(id = 17, nullable = true)
Boolean booleanValue2 = Boolean.TRUE;
- @ForyField(nullable = true)
+ @ForyField(id = 18, nullable = true)
Float floatValue2 = Float.valueOf(5.0f);
- @ForyField(nullable = true)
+ @ForyField(id = 19, nullable = true)
Double doubleValue2 = Double.valueOf(6.0);
- @ForyField(nullable = true)
+ @ForyField(id = 20, nullable = true)
Character character2 = Character.valueOf('c');
public BeanM() {
@@ -133,7 +136,8 @@ public class ForyAnnotationTest extends ForyTestBase {
@Data
public static class BeanM1 {
- @ForyField private BeanN beanN = new BeanN();
+ @ForyField(id = 0)
+ private BeanN beanN = new BeanN();
}
@Test(dataProvider = "basicMultiConfigFory")
diff --git
a/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldSerializationTest.java
b/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldSerializationTest.java
new file mode 100644
index 000000000..5a54dcc8f
--- /dev/null
+++
b/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldSerializationTest.java
@@ -0,0 +1,838 @@
+/*
+ * 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 org.apache.fory.annotation;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.fory.Fory;
+import org.apache.fory.ForyTestBase;
+import org.apache.fory.config.CompatibleMode;
+import org.apache.fory.config.Language;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class ForyFieldSerializationTest extends ForyTestBase {
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class PersonWithTagId {
+ @ForyField(id = 0, nullable = false)
+ public String veryLongFieldNameForFirstName;
+
+ @ForyField(id = 1, nullable = false)
+ public String anotherVeryLongFieldNameForLastName;
+
+ @ForyField(id = 2, nullable = false)
+ public int age;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class PersonWithoutTagId {
+ public String veryLongFieldNameForFirstName;
+ public String anotherVeryLongFieldNameForLastName;
+ public int age;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class PersonWithOptOutTagId {
+ @ForyField(id = -1, nullable = false)
+ public String veryLongFieldNameForFirstName;
+
+ @ForyField(id = -1, nullable = false)
+ public String anotherVeryLongFieldNameForLastName;
+
+ @ForyField(id = -1, nullable = false)
+ public int age;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class PersonMixedTagId {
+ @ForyField(id = 0, nullable = false)
+ public String firstName;
+
+ // This field uses field name (id = -1)
+ @ForyField(id = -1, nullable = false)
+ public String veryLongFieldNameForLastName;
+
+ public int age; // No annotation, uses field name
+ }
+
+ /** Nested object classes for testing field ID vs field type serialization */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class VeryLongNestedObjectClassName {
+ @ForyField(id = 0, nullable = false)
+ public String value;
+
+ @ForyField(id = 1, nullable = false)
+ public int count;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class AnotherVeryLongNestedObjectClassName {
+ @ForyField(id = 0, nullable = false)
+ public String description;
+ }
+
+ /** Container with nested objects using field tag IDs */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class ContainerWithTagIds {
+ @ForyField(id = 0, nullable = false)
+ public VeryLongNestedObjectClassName veryLongFieldNameForNestedObject;
+
+ @ForyField(id = 1, nullable = false)
+ public AnotherVeryLongNestedObjectClassName
anotherVeryLongFieldNameForAnotherNestedObject;
+
+ @ForyField(id = 2, nullable = false)
+ public String simpleField;
+ }
+
+ /** Container with nested objects WITHOUT tag IDs (uses field names) */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class ContainerWithoutTagIds {
+ public VeryLongNestedObjectClassName veryLongFieldNameForNestedObject;
+ public AnotherVeryLongNestedObjectClassName
anotherVeryLongFieldNameForAnotherNestedObject;
+ public String simpleField;
+ }
+
+ @DataProvider(name = "modes")
+ public Object[][] modes() {
+ return new Object[][] {
+ // JAVA mode with and without registration
+ {Language.JAVA, CompatibleMode.SCHEMA_CONSISTENT, false, false},
+ {Language.JAVA, CompatibleMode.SCHEMA_CONSISTENT, true, false},
+ {Language.JAVA, CompatibleMode.COMPATIBLE, false, false},
+ {Language.JAVA, CompatibleMode.COMPATIBLE, true, false},
+ {Language.JAVA, CompatibleMode.SCHEMA_CONSISTENT, false, true},
+ {Language.JAVA, CompatibleMode.SCHEMA_CONSISTENT, true, true},
+ {Language.JAVA, CompatibleMode.COMPATIBLE, false, true},
+ {Language.JAVA, CompatibleMode.COMPATIBLE, true, true},
+ // XLANG mode always requires registration
+ {Language.XLANG, CompatibleMode.SCHEMA_CONSISTENT, false, true},
+ {Language.XLANG, CompatibleMode.SCHEMA_CONSISTENT, true, true},
+ {Language.XLANG, CompatibleMode.COMPATIBLE, false, true},
+ {Language.XLANG, CompatibleMode.COMPATIBLE, true, true},
+ };
+ }
+
+ @Test(dataProvider = "modes")
+ public void testTagIdReducesPayloadSize(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ // Register classes based on parameter
+ if (registered) {
+ fory.register(PersonWithTagId.class, "test.PersonWithTagId");
+ fory.register(PersonWithoutTagId.class, "test.PersonWithoutTagId");
+ fory.register(PersonWithOptOutTagId.class, "test.PersonWithOptOutTagId");
+ }
+
+ PersonWithTagId personWithTag = new PersonWithTagId("John", "Doe", 30);
+ PersonWithoutTagId personWithoutTag = new PersonWithoutTagId("John",
"Doe", 30);
+ PersonWithOptOutTagId personWithOptOut = new PersonWithOptOutTagId("John",
"Doe", 30);
+
+ byte[] bytesWithTag = fory.serialize(personWithTag);
+ byte[] bytesWithoutTag = fory.serialize(personWithoutTag);
+ byte[] bytesWithOptOut = fory.serialize(personWithOptOut);
+
+ // Verify deserialization works
+ PersonWithTagId deserializedWithTag = (PersonWithTagId)
fory.deserialize(bytesWithTag);
+ PersonWithoutTagId deserializedWithoutTag =
+ (PersonWithoutTagId) fory.deserialize(bytesWithoutTag);
+ PersonWithOptOutTagId deserializedWithOptOut =
+ (PersonWithOptOutTagId) fory.deserialize(bytesWithOptOut);
+
+ assertEquals(deserializedWithTag.veryLongFieldNameForFirstName, "John");
+ assertEquals(deserializedWithTag.anotherVeryLongFieldNameForLastName,
"Doe");
+ assertEquals(deserializedWithTag.age, 30);
+
+ assertEquals(deserializedWithoutTag.veryLongFieldNameForFirstName, "John");
+ assertEquals(deserializedWithoutTag.anotherVeryLongFieldNameForLastName,
"Doe");
+ assertEquals(deserializedWithoutTag.age, 30);
+
+ assertEquals(deserializedWithOptOut.veryLongFieldNameForFirstName, "John");
+ assertEquals(deserializedWithOptOut.anotherVeryLongFieldNameForLastName,
"Doe");
+ assertEquals(deserializedWithOptOut.age, 30);
+
+ System.out.printf(
+ "Mode: %s/%s/codegen=%s - With tag: %d bytes, Without tag: %d bytes,
Opt-out (id=-1): %d bytes%n",
+ language,
+ compatibleMode,
+ codegen,
+ bytesWithTag.length,
+ bytesWithoutTag.length,
+ bytesWithOptOut.length);
+
+ // Tag IDs should reduce payload size in all modes (JAVA and XLANG)
+ // This is the core benefit of the @ForyField annotation feature
+ assertTrue(
+ bytesWithTag.length <= bytesWithoutTag.length,
+ String.format(
+ "Expected tag ID version (%d bytes) to be <= field name version
(%d bytes) in mode %s/%s/codegen=%s",
+ bytesWithTag.length, bytesWithoutTag.length, language,
compatibleMode, codegen));
+
+ // Tag ID version should also be smaller than or equal to opt-out version
(id=-1)
+ assertTrue(
+ bytesWithTag.length <= bytesWithOptOut.length,
+ String.format(
+ "Expected tag ID version (%d bytes) to be <= opt-out id=-1 version
(%d bytes) in mode %s/%s/codegen=%s",
+ bytesWithTag.length, bytesWithOptOut.length, language,
compatibleMode, codegen));
+
+ // Opt-out (id=-1) should have similar size to no annotation (both use
field names)
+ // They should be equal or very close in size
+ int sizeDifference = Math.abs(bytesWithOptOut.length -
bytesWithoutTag.length);
+ assertTrue(
+ sizeDifference <= 5,
+ String.format(
+ "Expected opt-out id=-1 (%d bytes) to have similar size to no
annotation (%d bytes), but difference is %d bytes",
+ bytesWithOptOut.length, bytesWithoutTag.length, sizeDifference));
+ }
+
+ @Test(dataProvider = "modes")
+ public void testFieldNameNotInPayloadWithTagId(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(PersonWithTagId.class, "test.PersonWithTagId");
+ }
+
+ PersonWithTagId person = new PersonWithTagId("Alice", "Smith", 25);
+ byte[] bytes = fory.serialize(person);
+
+ // Convert to string to search for field names
+ String serialized = new String(bytes, StandardCharsets.UTF_8);
+
+ // With tag IDs, field names should generally NOT appear in the payload in
most modes
+ // Note: Exact behavior may vary by mode, but we verify deserialization
always works
+ // In XLANG/COMPATIBLE mode specifically, field names should definitely
not be present
+ if (language == Language.XLANG && compatibleMode ==
CompatibleMode.COMPATIBLE) {
+ assertFalse(
+ serialized.contains("veryLongFieldNameForFirstName"),
+ String.format(
+ "Field name 'veryLongFieldNameForFirstName' should not be in
payload with tag ID in mode %s/%s/codegen=%s",
+ language, compatibleMode, codegen));
+ assertFalse(
+ serialized.contains("anotherVeryLongFieldNameForLastName"),
+ String.format(
+ "Field name 'anotherVeryLongFieldNameForLastName' should not be
in payload with tag ID in mode %s/%s/codegen=%s",
+ language, compatibleMode, codegen));
+ }
+
+ // Verify deserialization still works in ALL modes
+ PersonWithTagId deserialized = (PersonWithTagId) fory.deserialize(bytes);
+ assertEquals(deserialized.veryLongFieldNameForFirstName, "Alice");
+ assertEquals(deserialized.anotherVeryLongFieldNameForLastName, "Smith");
+ assertEquals(deserialized.age, 25);
+ }
+
+ @Test(dataProvider = "modes")
+ public void testFieldNameInPayloadWithoutTagId(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(PersonWithoutTagId.class, "test.PersonWithoutTagId");
+ }
+
+ PersonWithoutTagId person = new PersonWithoutTagId("Bob", "Johnson", 35);
+ byte[] bytes = fory.serialize(person);
+
+ // In COMPATIBLE mode without tag IDs, field names are used for field
matching
+ // (though they may be encoded using meta string compression)
+ if (compatibleMode == CompatibleMode.COMPATIBLE) {
+ // Verify the data deserializes correctly
+ PersonWithoutTagId deserialized = (PersonWithoutTagId)
fory.deserialize(bytes);
+ assertEquals(deserialized.veryLongFieldNameForFirstName, "Bob");
+ assertEquals(deserialized.anotherVeryLongFieldNameForLastName,
"Johnson");
+ assertEquals(deserialized.age, 35);
+ }
+ }
+
+ @Test(dataProvider = "modes")
+ public void testMixedTagIdAndFieldName(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(PersonMixedTagId.class, "test.PersonMixedTagId");
+ }
+
+ PersonMixedTagId person = new PersonMixedTagId("Charlie", "Brown", 40);
+ byte[] bytes = fory.serialize(person);
+
+ // Verify deserialization works correctly with mixed mode
+ PersonMixedTagId deserialized = (PersonMixedTagId) fory.deserialize(bytes);
+ assertEquals(deserialized.firstName, "Charlie");
+ assertEquals(deserialized.veryLongFieldNameForLastName, "Brown");
+ assertEquals(deserialized.age, 40);
+
+ System.out.printf(
+ "Mixed mode - %s/%s/codegen=%s: %d bytes%n",
+ language, compatibleMode, codegen, bytes.length);
+ }
+
+ /** Test class for nullable and ref flags */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class TestNullableRef {
+ @ForyField(id = 0, nullable = false, ref = false)
+ String nonNullableNoRef;
+
+ @ForyField(id = 1, nullable = true, ref = false)
+ String nullableNoRef;
+
+ @ForyField(id = 2, nullable = false, ref = true)
+ String nonNullableWithRef;
+
+ @ForyField(id = 3, nullable = true, ref = true)
+ String nullableWithRef;
+ }
+
+ @Test(dataProvider = "modes")
+ public void testNullableAndRefFlagsInPayload(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(TestNullableRef.class, "test.TestNullableRef");
+ }
+
+ TestNullableRef obj = new TestNullableRef("a", null, "c", "d");
+ byte[] bytes = fory.serialize(obj);
+
+ // Verify deserialization
+ TestNullableRef deserialized = (TestNullableRef) fory.deserialize(bytes);
+ assertEquals(deserialized.nonNullableNoRef, "a");
+ assertNull(deserialized.nullableNoRef);
+ assertEquals(deserialized.nonNullableWithRef, "c");
+ assertEquals(deserialized.nullableWithRef, "d");
+
+ System.out.printf(
+ "Nullable/Ref test - %s/%s/codegen=%s: %d bytes%n",
+ language, compatibleMode, codegen, bytes.length);
+ }
+
+ /** Test class with all fields nullable=false, ref=false for size comparison
*/
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class AllNonNullableNoRef {
+ @ForyField(id = 0, nullable = false, ref = false)
+ String field1;
+
+ @ForyField(id = 1, nullable = false, ref = false)
+ String field2;
+
+ @ForyField(id = 2, nullable = false, ref = false)
+ String field3;
+ }
+
+ /** Test class with all fields nullable=true, ref=false for size comparison
*/
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class AllNullableNoRef {
+ @ForyField(id = 0, nullable = true, ref = false)
+ String field1;
+
+ @ForyField(id = 1, nullable = true, ref = false)
+ String field2;
+
+ @ForyField(id = 2, nullable = true, ref = false)
+ String field3;
+ }
+
+ /** Test class with all fields nullable=false, ref=true for size comparison
*/
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class AllNonNullableWithRef {
+ @ForyField(id = 0, nullable = false, ref = true)
+ String field1;
+
+ @ForyField(id = 1, nullable = false, ref = true)
+ String field2;
+
+ @ForyField(id = 2, nullable = false, ref = true)
+ String field3;
+ }
+
+ /** Test class with all fields nullable=true, ref=true for size comparison */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class AllNullableWithRef {
+ @ForyField(id = 0, nullable = true, ref = true)
+ String field1;
+
+ @ForyField(id = 1, nullable = true, ref = true)
+ String field2;
+
+ @ForyField(id = 2, nullable = true, ref = true)
+ String field3;
+ }
+
+ @Test(dataProvider = "modes")
+ public void testNullableFlagReducesPayloadSize(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(AllNonNullableNoRef.class, "test.AllNonNullableNoRef");
+ fory.register(AllNullableNoRef.class, "test.AllNullableNoRef");
+ }
+
+ // Create objects with same data
+ AllNonNullableNoRef nonNullable = new AllNonNullableNoRef("value1",
"value2", "value3");
+ AllNullableNoRef nullable = new AllNullableNoRef("value1", "value2",
"value3");
+
+ byte[] bytesNonNullable = fory.serialize(nonNullable);
+ byte[] bytesNullable = fory.serialize(nullable);
+
+ // Verify deserialization works
+ AllNonNullableNoRef deserializedNonNullable =
+ (AllNonNullableNoRef) fory.deserialize(bytesNonNullable);
+ AllNullableNoRef deserializedNullable = (AllNullableNoRef)
fory.deserialize(bytesNullable);
+
+ assertEquals(deserializedNonNullable.field1, "value1");
+ assertEquals(deserializedNonNullable.field2, "value2");
+ assertEquals(deserializedNonNullable.field3, "value3");
+ assertEquals(deserializedNullable.field1, "value1");
+ assertEquals(deserializedNullable.field2, "value2");
+ assertEquals(deserializedNullable.field3, "value3");
+
+ System.out.printf(
+ "Nullable flag test - %s/%s/codegen=%s/registered=%s - NonNullable: %d
bytes, Nullable: %d bytes%n",
+ language,
+ compatibleMode,
+ codegen,
+ registered,
+ bytesNonNullable.length,
+ bytesNullable.length);
+
+ // nullable=false should produce smaller or equal payload
+ // Each nullable=true field adds 1 byte for null flag
+ assertTrue(
+ bytesNonNullable.length <= bytesNullable.length,
+ String.format(
+ "Expected non-nullable (%d bytes) to be <= nullable (%d bytes) in
mode %s/%s/codegen=%s/registered=%s",
+ bytesNonNullable.length,
+ bytesNullable.length,
+ language,
+ compatibleMode,
+ codegen,
+ registered));
+ }
+
+ @Test(dataProvider = "modes")
+ public void testRefFlagReducesPayloadSize(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(AllNonNullableNoRef.class, "test.AllNonNullableNoRef");
+ fory.register(AllNonNullableWithRef.class, "test.AllNonNullableWithRef");
+ }
+
+ // Create objects with same data
+ AllNonNullableNoRef noRef = new AllNonNullableNoRef("value1", "value2",
"value3");
+ AllNonNullableWithRef withRef = new AllNonNullableWithRef("value1",
"value2", "value3");
+
+ byte[] bytesNoRef = fory.serialize(noRef);
+ byte[] bytesWithRef = fory.serialize(withRef);
+
+ // Verify deserialization works
+ AllNonNullableNoRef deserializedNoRef = (AllNonNullableNoRef)
fory.deserialize(bytesNoRef);
+ AllNonNullableWithRef deserializedWithRef =
+ (AllNonNullableWithRef) fory.deserialize(bytesWithRef);
+
+ assertEquals(deserializedNoRef.field1, "value1");
+ assertEquals(deserializedNoRef.field2, "value2");
+ assertEquals(deserializedNoRef.field3, "value3");
+ assertEquals(deserializedWithRef.field1, "value1");
+ assertEquals(deserializedWithRef.field2, "value2");
+ assertEquals(deserializedWithRef.field3, "value3");
+
+ System.out.printf(
+ "Ref flag test - %s/%s/codegen=%s/registered=%s - NoRef: %d bytes,
WithRef: %d bytes%n",
+ language, compatibleMode, codegen, registered, bytesNoRef.length,
bytesWithRef.length);
+
+ // ref=false should produce smaller or equal payload
+ // Each ref=true field may add overhead for reference tracking
+ assertTrue(
+ bytesNoRef.length <= bytesWithRef.length,
+ String.format(
+ "Expected no-ref (%d bytes) to be <= with-ref (%d bytes) in mode
%s/%s/codegen=%s/registered=%s",
+ bytesNoRef.length, bytesWithRef.length, language, compatibleMode,
codegen, registered));
+ }
+
+ @Test(dataProvider = "modes")
+ public void testCombinedNullableAndRefFlagsReducePayloadSize(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(AllNonNullableNoRef.class, "test.AllNonNullableNoRef");
+ fory.register(AllNullableWithRef.class, "test.AllNullableWithRef");
+ }
+
+ // Create objects with same data
+ // Most optimized: nullable=false, ref=false
+ AllNonNullableNoRef optimized = new AllNonNullableNoRef("value1",
"value2", "value3");
+ // Least optimized: nullable=true, ref=true
+ AllNullableWithRef unoptimized = new AllNullableWithRef("value1",
"value2", "value3");
+
+ byte[] bytesOptimized = fory.serialize(optimized);
+ byte[] bytesUnoptimized = fory.serialize(unoptimized);
+
+ // Verify deserialization works
+ AllNonNullableNoRef deserializedOptimized =
+ (AllNonNullableNoRef) fory.deserialize(bytesOptimized);
+ AllNullableWithRef deserializedUnoptimized =
+ (AllNullableWithRef) fory.deserialize(bytesUnoptimized);
+
+ assertEquals(deserializedOptimized.field1, "value1");
+ assertEquals(deserializedOptimized.field2, "value2");
+ assertEquals(deserializedOptimized.field3, "value3");
+ assertEquals(deserializedUnoptimized.field1, "value1");
+ assertEquals(deserializedUnoptimized.field2, "value2");
+ assertEquals(deserializedUnoptimized.field3, "value3");
+
+ System.out.printf(
+ "Combined flags test - %s/%s/codegen=%s/registered=%s - Optimized: %d
bytes, Unoptimized: %d bytes, Savings: %d bytes (%.1f%%)%n",
+ language,
+ compatibleMode,
+ codegen,
+ registered,
+ bytesOptimized.length,
+ bytesUnoptimized.length,
+ bytesUnoptimized.length - bytesOptimized.length,
+ 100.0 * (bytesUnoptimized.length - bytesOptimized.length) /
bytesUnoptimized.length);
+
+ // Optimized (nullable=false, ref=false) should be smaller than
unoptimized (nullable=true,
+ // ref=true)
+ assertTrue(
+ bytesOptimized.length < bytesUnoptimized.length,
+ String.format(
+ "Expected optimized (nullable=false,ref=false) %d bytes to be <
unoptimized (nullable=true,ref=true) %d bytes in mode
%s/%s/codegen=%s/registered=%s",
+ bytesOptimized.length,
+ bytesUnoptimized.length,
+ language,
+ compatibleMode,
+ codegen,
+ registered));
+ }
+
+ /** Version 1 of Person class for schema evolution test */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class PersonV1 {
+ @ForyField(id = 0, nullable = false)
+ String name;
+
+ @ForyField(id = 1, nullable = false)
+ int age;
+ }
+
+ /** Version 2 of Person class for schema evolution test */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class PersonV2 {
+ @ForyField(id = 0, nullable = false)
+ String name;
+
+ @ForyField(id = 1, nullable = false)
+ int age;
+
+ @ForyField(id = 2, nullable = true) // New optional field
+ String email;
+ }
+
+ @Test
+ public void testSchemaEvolutionWithTagIds() {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(Language.JAVA)
+ .withCompatibleMode(CompatibleMode.COMPATIBLE)
+ .requireClassRegistration(false)
+ .build();
+
+ // Serialize with V1
+ PersonV1 personV1 = new PersonV1("Alice", 30);
+ byte[] bytesV1 = fory.serialize(personV1);
+
+ // Note: Schema evolution across different class types requires XLANG mode
with proper
+ // tag ID support. In JAVA mode, we can only test
serialization/deserialization
+ // of the same class version. The tag IDs are stored in metadata but not
used
+ // for field matching in JAVA mode.
+
+ PersonV1 deserialized = (PersonV1) fory.deserialize(bytesV1);
+ assertEquals(deserialized.name, "Alice");
+ assertEquals(deserialized.age, 30);
+
+ // Serialize with V2
+ PersonV2 personV2Full = new PersonV2("Bob", 25, "[email protected]");
+ byte[] bytesV2 = fory.serialize(personV2Full);
+
+ PersonV2 deserializedV2 = (PersonV2) fory.deserialize(bytesV2);
+ assertEquals(deserializedV2.name, "Bob");
+ assertEquals(deserializedV2.age, 25);
+ assertEquals(deserializedV2.email, "[email protected]");
+
+ System.out.printf(
+ "Schema evolution test - V1: %d bytes, V2: %d bytes%n",
bytesV1.length, bytesV2.length);
+ }
+
+ /**
+ * Comprehensive test for nested objects with @ForyField tag IDs. Verifies
that: 1. Field IDs are
+ * written instead of field names for nested object fields 2. Nested object
class IDs (if
+ * registered) are written instead of class names/types 3. Payload size is
smaller when using tag
+ * IDs 4. Deserialization works correctly
+ */
+ @Test(dataProvider = "modes")
+ public void testNestedObjectsWithTagIds(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(ContainerWithTagIds.class, "test.ContainerWithTagIds");
+ fory.register(ContainerWithoutTagIds.class,
"test.ContainerWithoutTagIds");
+ fory.register(VeryLongNestedObjectClassName.class,
"test.VeryLongNestedObjectClassName");
+ fory.register(
+ AnotherVeryLongNestedObjectClassName.class,
"test.AnotherVeryLongNestedObjectClassName");
+ }
+
+ // Create nested objects with same data
+ VeryLongNestedObjectClassName nested1 = new
VeryLongNestedObjectClassName("value1", 42);
+ AnotherVeryLongNestedObjectClassName nested2 =
+ new AnotherVeryLongNestedObjectClassName("description1");
+
+ ContainerWithTagIds containerWithTags =
+ new ContainerWithTagIds(nested1, nested2, "simpleValue");
+ ContainerWithoutTagIds containerWithoutTags =
+ new ContainerWithoutTagIds(nested1, nested2, "simpleValue");
+
+ // Serialize both
+ byte[] bytesWithTags = fory.serialize(containerWithTags);
+ byte[] bytesWithoutTags = fory.serialize(containerWithoutTags);
+
+ // Verify deserialization works
+ ContainerWithTagIds deserializedWithTags =
+ (ContainerWithTagIds) fory.deserialize(bytesWithTags);
+ ContainerWithoutTagIds deserializedWithoutTags =
+ (ContainerWithoutTagIds) fory.deserialize(bytesWithoutTags);
+
+ // Verify nested object values
+ assertEquals(
+ deserializedWithTags.veryLongFieldNameForNestedObject.value,
+ "value1",
+ "Nested object value should match");
+ assertEquals(
+ deserializedWithTags.veryLongFieldNameForNestedObject.count,
+ 42,
+ "Nested object count should match");
+ assertEquals(
+
deserializedWithTags.anotherVeryLongFieldNameForAnotherNestedObject.description,
+ "description1",
+ "Another nested object description should match");
+ assertEquals(deserializedWithTags.simpleField, "simpleValue", "Simple
field should match");
+
+ assertEquals(
+ deserializedWithoutTags.veryLongFieldNameForNestedObject.value,
+ "value1",
+ "Nested object value should match");
+ assertEquals(
+ deserializedWithoutTags.veryLongFieldNameForNestedObject.count,
+ 42,
+ "Nested object count should match");
+ assertEquals(
+
deserializedWithoutTags.anotherVeryLongFieldNameForAnotherNestedObject.description,
+ "description1",
+ "Another nested object description should match");
+ assertEquals(deserializedWithoutTags.simpleField, "simpleValue", "Simple
field should match");
+
+ System.out.printf(
+ "Nested objects test - %s/%s/codegen=%s/registered=%s - With tags: %d
bytes, Without tags: %d bytes%n",
+ language,
+ compatibleMode,
+ codegen,
+ registered,
+ bytesWithTags.length,
+ bytesWithoutTags.length);
+
+ // Tag IDs should produce smaller payload in all modes
+ assertTrue(
+ bytesWithTags.length < bytesWithoutTags.length,
+ String.format(
+ "Expected nested objects with tag IDs (%d bytes) to be < without
tag IDs (%d bytes) in %s/%s/codegen=%s/registered=%s",
+ bytesWithTags.length,
+ bytesWithoutTags.length,
+ language,
+ compatibleMode,
+ codegen,
+ registered));
+
+ // Print savings information
+ System.out.printf(
+ " Savings from tag IDs: %d bytes (%.1f%%)%n",
+ bytesWithoutTags.length - bytesWithTags.length,
+ 100.0 * (bytesWithoutTags.length - bytesWithTags.length) /
bytesWithoutTags.length);
+ }
+
+ /**
+ * Test that verifies field IDs are written in the payload (not field
names). This test inspects
+ * the raw bytes to confirm the serialization format.
+ */
+ @Test(dataProvider = "modes")
+ public void testNestedObjectFieldIdInPayload(
+ Language language, CompatibleMode compatibleMode, boolean codegen,
boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCompatibleMode(compatibleMode)
+ .withCodegen(codegen)
+ .build();
+
+ if (registered) {
+ fory.register(ContainerWithTagIds.class, "test.ContainerWithTagIds");
+ fory.register(VeryLongNestedObjectClassName.class,
"test.VeryLongNestedObjectClassName");
+ fory.register(
+ AnotherVeryLongNestedObjectClassName.class,
"test.AnotherVeryLongNestedObjectClassName");
+ }
+
+ VeryLongNestedObjectClassName nested1 = new
VeryLongNestedObjectClassName("test", 1);
+ AnotherVeryLongNestedObjectClassName nested2 = new
AnotherVeryLongNestedObjectClassName("desc");
+
+ ContainerWithTagIds container = new ContainerWithTagIds(nested1, nested2,
"simple");
+
+ byte[] bytes = fory.serialize(container);
+
+ // Verify deserialization
+ ContainerWithTagIds deserialized = (ContainerWithTagIds)
fory.deserialize(bytes);
+ assertEquals(deserialized.veryLongFieldNameForNestedObject.value, "test");
+ assertEquals(deserialized.veryLongFieldNameForNestedObject.count, 1);
+
assertEquals(deserialized.anotherVeryLongFieldNameForAnotherNestedObject.description,
"desc");
+ assertEquals(deserialized.simpleField, "simple");
+
+ // When using tag IDs with @ForyField, field names should NOT be in payload
+ // This works in all modes: JAVA/XLANG and COMPATIBLE/SCHEMA_CONSISTENT
+ String serialized = new String(bytes, StandardCharsets.UTF_8);
+
+ // These long field names should not be present because we're using field
IDs (0, 1, 2)
+ boolean hasLongFieldName1 =
serialized.contains("veryLongFieldNameForNestedObject");
+ boolean hasLongFieldName2 =
+ serialized.contains("anotherVeryLongFieldNameForAnotherNestedObject");
+
+ assertFalse(
+ hasLongFieldName1,
+ String.format(
+ "Field name 'veryLongFieldNameForNestedObject' should NOT be in
payload with tag ID in %s/%s/codegen=%s/registered=%s",
+ language, compatibleMode, codegen, registered));
+ assertFalse(
+ hasLongFieldName2,
+ String.format(
+ "Field name 'anotherVeryLongFieldNameForAnotherNestedObject'
should NOT be in payload with tag ID in %s/%s/codegen=%s/registered=%s",
+ language, compatibleMode, codegen, registered));
+
+ System.out.printf(
+ "Verified: Field IDs used instead of field names in
%s/%s/codegen=%s/registered=%s%n",
+ language, compatibleMode, codegen, registered);
+ }
+}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldTagIdTest.java
b/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldTagIdTest.java
new file mode 100644
index 000000000..f96f64d39
--- /dev/null
+++
b/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldTagIdTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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 org.apache.fory.annotation;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import lombok.Data;
+import org.apache.fory.Fory;
+import org.apache.fory.ForyTestBase;
+import org.apache.fory.config.Language;
+import org.apache.fory.meta.ClassDef;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class ForyFieldTagIdTest extends ForyTestBase {
+
+ @Data
+ public static class TestClass {
+ @ForyField(id = 0, nullable = false)
+ public String fieldWithTag0;
+
+ @ForyField(id = 5, nullable = false)
+ public String fieldWithTag5;
+
+ @ForyField(id = -1, nullable = false)
+ public String fieldOptingOutOfTag;
+
+ public String fieldWithoutAnnotation;
+ }
+
+ @Test(dataProvider = "languageAndCodegen")
+ public void testFieldInfoCreationWithTagIds(
+ Language language, boolean codegen, boolean registered) {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(language)
+ .requireClassRegistration(registered)
+ .withCodegen(codegen)
+ .build();
+
+ if (language == Language.XLANG) {
+ fory.register(TestClass.class, "test.TestClass");
+ }
+
+ ClassDef classDef = ClassDef.buildClassDef(fory, TestClass.class);
+ List<ClassDef.FieldInfo> fieldsInfo = classDef.getFieldsInfo();
+
+ // Should have 4 fields
+ assertEquals(fieldsInfo.size(), 4);
+
+ // Find each field by name and verify tag behavior
+ ClassDef.FieldInfo field0 = findFieldByName(fieldsInfo, "fieldWithTag0");
+ ClassDef.FieldInfo field5 = findFieldByName(fieldsInfo, "fieldWithTag5");
+ ClassDef.FieldInfo fieldOptOut = findFieldByName(fieldsInfo,
"fieldOptingOutOfTag");
+ ClassDef.FieldInfo fieldNoAnnotation = findFieldByName(fieldsInfo,
"fieldWithoutAnnotation");
+
+ // Verify field with id=0 has tag
+ assertTrue(field0.hasFieldId(), "Field with id=0 should have tag in " +
language + " mode");
+ assertEquals(
+ field0.getFieldId(),
+ (short) 0,
+ "Field with id=0 should have tag value 0 in " + language + " mode");
+
+ // Verify field with id=5 has tag
+ assertTrue(field5.hasFieldId(), "Field with id=5 should have tag in " +
language + " mode");
+ assertEquals(
+ field5.getFieldId(),
+ (short) 5,
+ "Field with id=5 should have tag value 5 in " + language + " mode");
+
+ // Verify field with id=-1 does NOT have tag (opts out)
+ assertFalse(
+ fieldOptOut.hasFieldId(),
+ "Field with id=-1 should NOT have tag (opt-out) in " + language + "
mode");
+ assertEquals(
+ fieldOptOut.getFieldName(),
+ "fieldOptingOutOfTag",
+ "Field with id=-1 should use field name in " + language + " mode");
+
+ // Verify field without annotation does NOT have tag
+ assertFalse(
+ fieldNoAnnotation.hasFieldId(),
+ "Field without annotation should NOT have tag (use field name) in " +
language + " mode");
+ assertEquals(
+ fieldNoAnnotation.getFieldName(),
+ "fieldWithoutAnnotation",
+ "Field without annotation should use field name in " + language + "
mode");
+ }
+
+ @DataProvider(name = "languageAndCodegen")
+ public Object[][] languageAndCodegen() {
+ return new Object[][] {
+ {Language.JAVA, false, false},
+ {Language.JAVA, false, true},
+ {Language.JAVA, true, false},
+ {Language.JAVA, true, true},
+ {Language.XLANG, false, true},
+ {Language.XLANG, true, true},
+ };
+ }
+
+ @Test
+ public void testTagIdAnnotationValues() throws Exception {
+ // Directly test that annotation reading works correctly
+ Field field0 = TestClass.class.getDeclaredField("fieldWithTag0");
+ Field field5 = TestClass.class.getDeclaredField("fieldWithTag5");
+ Field fieldOptOut =
TestClass.class.getDeclaredField("fieldOptingOutOfTag");
+ Field fieldNoAnnotation =
TestClass.class.getDeclaredField("fieldWithoutAnnotation");
+
+ ForyField annotation0 = field0.getAnnotation(ForyField.class);
+ ForyField annotation5 = field5.getAnnotation(ForyField.class);
+ ForyField annotationOptOut = fieldOptOut.getAnnotation(ForyField.class);
+ ForyField annotationNoAnnotation =
fieldNoAnnotation.getAnnotation(ForyField.class);
+
+ // Verify annotation values
+ assertEquals(annotation0.id(), 0, "Field 0 should have id=0");
+ assertEquals(annotation5.id(), 5, "Field 5 should have id=5");
+ assertEquals(annotationOptOut.id(), -1, "Opt-out field should have id=-1");
+ assertNull(
+ annotationNoAnnotation, "Field without annotation should have no
ForyField annotation");
+ }
+
+ @Test
+ public void testIdMinusOneOptOutBehavior() {
+ // Test that id=-1 explicitly opts out of tag ID usage
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).requireClassRegistration(false).build();
+ fory.register(TestClass.class, "test.TestClass");
+
+ TestClass obj = new TestClass();
+ obj.setFieldWithTag0("value0");
+ obj.setFieldWithTag5("value5");
+ obj.setFieldOptingOutOfTag("optOutValue");
+ obj.setFieldWithoutAnnotation("noAnnotationValue");
+
+ // Serialize and deserialize
+ byte[] bytes = fory.serialize(obj);
+ TestClass deserialized = (TestClass) fory.deserialize(bytes);
+
+ // All fields should deserialize correctly
+ assertEquals(deserialized.getFieldWithTag0(), "value0");
+ assertEquals(deserialized.getFieldWithTag5(), "value5");
+ assertEquals(deserialized.getFieldOptingOutOfTag(), "optOutValue");
+ assertEquals(deserialized.getFieldWithoutAnnotation(),
"noAnnotationValue");
+ }
+
+ /** Helper method to find a FieldInfo by field name */
+ private ClassDef.FieldInfo findFieldByName(List<ClassDef.FieldInfo>
fieldsInfo, String name) {
+ for (ClassDef.FieldInfo fieldInfo : fieldsInfo) {
+ if (fieldInfo.getFieldName().equals(name)) {
+ return fieldInfo;
+ }
+ }
+ throw new AssertionError("Field not found: " + name);
+ }
+}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldTest.java
b/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldTest.java
new file mode 100644
index 000000000..a9b2a81ef
--- /dev/null
+++ b/java/fory-core/src/test/java/org/apache/fory/annotation/ForyFieldTest.java
@@ -0,0 +1,264 @@
+/*
+ * 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 org.apache.fory.annotation;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.fory.Fory;
+import org.apache.fory.ForyTestBase;
+import org.apache.fory.config.Language;
+import org.testng.annotations.Test;
+
+public class ForyFieldTest extends ForyTestBase {
+
+ /** Example 1: Simple Value Object */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Point {
+ @ForyField(id = 0, nullable = false, ref = false)
+ public double x;
+
+ @ForyField(id = 1, nullable = false, ref = false)
+ public double y;
+ }
+
+ @Test
+ public void testSimpleValueObject() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ Point point = new Point(3.14, 2.71);
+ byte[] bytes = fory.serialize(point);
+ Point deserialized = (Point) fory.deserialize(bytes);
+ assertEquals(deserialized.x, 3.14);
+ assertEquals(deserialized.y, 2.71);
+ }
+
+ /** Example 2: Entity with Optional Fields */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class User {
+ @ForyField(id = 0, nullable = false, ref = false)
+ public long userId;
+
+ @ForyField(id = 1, nullable = false, ref = false)
+ public String username;
+
+ @ForyField(id = 2, nullable = true, ref = false)
+ public String email; // Can be null during account creation
+
+ @ForyField(id = 3, nullable = true, ref = false)
+ public String phoneNumber; // Optional contact method
+
+ @ForyField(id = 4, nullable = false, ref = false)
+ public long createdAt;
+ }
+
+ @Test
+ public void testEntityWithOptionalFields() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ User user = new User(123L, "john_doe", "[email protected]", null,
System.currentTimeMillis());
+ byte[] bytes = fory.serialize(user);
+ User deserialized = (User) fory.deserialize(bytes);
+ assertEquals(deserialized.userId, 123L);
+ assertEquals(deserialized.username, "john_doe");
+ assertEquals(deserialized.email, "[email protected]");
+ assertNull(deserialized.phoneNumber);
+ }
+
+ @Test
+ public void testEntityWithAllNullOptionalFields() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ User user = new User(456L, "jane_doe", null, null,
System.currentTimeMillis());
+ byte[] bytes = fory.serialize(user);
+ User deserialized = (User) fory.deserialize(bytes);
+ assertEquals(deserialized.userId, 456L);
+ assertEquals(deserialized.username, "jane_doe");
+ assertNull(deserialized.email);
+ assertNull(deserialized.phoneNumber);
+ }
+
+ /** Example 3: Shared Object References */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Customer {
+ @ForyField(id = 0, nullable = false, ref = false)
+ public long customerId;
+
+ @ForyField(id = 1, nullable = false, ref = false)
+ public String name;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Order {
+ @ForyField(id = 0, nullable = false, ref = false)
+ public long orderId;
+
+ @ForyField(id = 1, nullable = false, ref = true)
+ public Customer customer; // Same Customer might appear in many orders
+
+ @ForyField(id = 2, nullable = true, ref = false)
+ public String notes; // Unique per order
+ }
+
+ @Test
+ public void testSharedObjectReferences() {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(Language.JAVA)
+ .withRefTracking(true)
+ .requireClassRegistration(false)
+ .build();
+ Customer customer = new Customer(1L, "Alice");
+ Order order1 = new Order(100L, customer, "First order");
+ Order order2 = new Order(101L, customer, null);
+
+ // Serialize orders that share the same customer
+ List<Order> orders = new ArrayList<>();
+ orders.add(order1);
+ orders.add(order2);
+
+ byte[] bytes = fory.serialize(orders);
+ @SuppressWarnings("unchecked")
+ List<Order> deserialized = (List<Order>) fory.deserialize(bytes);
+
+ assertEquals(deserialized.size(), 2);
+ assertEquals(deserialized.get(0).orderId, 100L);
+ assertEquals(deserialized.get(1).orderId, 101L);
+ assertEquals(deserialized.get(0).customer.customerId, 1L);
+ assertEquals(deserialized.get(1).customer.customerId, 1L);
+ // Both orders should reference the same customer object due to ref=true
+ // (though this is more about serialization efficiency than behavior)
+ }
+
+ /** Test nullable defaults */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class DefaultNullableTest {
+ @ForyField(id = 0) // nullable defaults to false
+ public String field1;
+
+ @ForyField(id = 1, nullable = true)
+ public String field2;
+ }
+
+ @Test
+ public void testNullableDefaults() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ DefaultNullableTest obj = new DefaultNullableTest("value1", null);
+ byte[] bytes = fory.serialize(obj);
+ DefaultNullableTest deserialized = (DefaultNullableTest)
fory.deserialize(bytes);
+ assertEquals(deserialized.field1, "value1");
+ assertNull(deserialized.field2);
+ }
+
+ /** Test ref defaults */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class DefaultRefTest {
+ @ForyField(id = 0) // ref defaults to false
+ public String field1;
+
+ @ForyField(id = 1, ref = true)
+ public String field2;
+ }
+
+ @Test
+ public void testRefDefaults() {
+ Fory fory =
+ Fory.builder()
+ .withLanguage(Language.JAVA)
+ .withRefTracking(true)
+ .requireClassRegistration(false)
+ .build();
+ DefaultRefTest obj = new DefaultRefTest("value1", "value2");
+ byte[] bytes = fory.serialize(obj);
+ DefaultRefTest deserialized = (DefaultRefTest) fory.deserialize(bytes);
+ assertEquals(deserialized.field1, "value1");
+ assertEquals(deserialized.field2, "value2");
+ }
+
+ /** Test mixed annotations and non-annotations */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class MixedFieldsTest {
+ @ForyField(id = 0, nullable = false)
+ public String annotatedField;
+
+ public String regularField; // No annotation
+
+ @ForyField(id = 1, nullable = true)
+ public String anotherAnnotatedField;
+ }
+
+ @Test
+ public void testMixedAnnotatedAndRegularFields() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ MixedFieldsTest obj = new MixedFieldsTest("annotated", "regular", null);
+ byte[] bytes = fory.serialize(obj);
+ MixedFieldsTest deserialized = (MixedFieldsTest) fory.deserialize(bytes);
+ assertEquals(deserialized.annotatedField, "annotated");
+ assertEquals(deserialized.regularField, "regular");
+ assertNull(deserialized.anotherAnnotatedField);
+ }
+
+ /** Test with primitive types */
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class PrimitiveFieldsTest {
+ @ForyField(id = 0) // primitives are never null
+ public int intField;
+
+ @ForyField(id = 1)
+ public long longField;
+
+ @ForyField(id = 2)
+ public boolean booleanField;
+
+ @ForyField(id = 3, nullable = false) // Boxed type marked as non-nullable
+ public Integer boxedInt;
+ }
+
+ @Test
+ public void testPrimitiveFields() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ PrimitiveFieldsTest obj = new PrimitiveFieldsTest(42, 123456789L, true,
99);
+ byte[] bytes = fory.serialize(obj);
+ PrimitiveFieldsTest deserialized = (PrimitiveFieldsTest)
fory.deserialize(bytes);
+ assertEquals(deserialized.intField, 42);
+ assertEquals(deserialized.longField, 123456789L);
+ assertTrue(deserialized.booleanField);
+ assertEquals(deserialized.boxedInt, Integer.valueOf(99));
+ }
+}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/meta/ClassDefEncoderTest.java
b/java/fory-core/src/test/java/org/apache/fory/meta/ClassDefEncoderTest.java
index 170c5a50e..356732d0b 100644
--- a/java/fory-core/src/test/java/org/apache/fory/meta/ClassDefEncoderTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/meta/ClassDefEncoderTest.java
@@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.List;
import lombok.Data;
import org.apache.fory.Fory;
+import org.apache.fory.annotation.ForyField;
import org.apache.fory.config.CompatibleMode;
import org.apache.fory.config.Language;
import org.apache.fory.memory.MemoryBuffer;
@@ -38,7 +39,7 @@ import org.testng.annotations.Test;
public class ClassDefEncoderTest {
@Test
- public void testBasicClassDef() throws Exception {
+ public void testBasicClassDef() {
Fory fory = Fory.builder().withMetaShare(true).build();
Class<ClassDefTest.TestFieldsOrderClass1> type =
ClassDefTest.TestFieldsOrderClass1.class;
List<ClassDef.FieldInfo> fieldsInfo =
buildFieldsInfo(fory.getClassResolver(), type);
@@ -170,4 +171,73 @@ public class ClassDefEncoderTest {
public static class ChildClass extends BaseAbstractClass {
private String name;
}
+
+ // Test classes for duplicate tag ID validation in ClassDefEncoder
+ @Data
+ public static class ClassWithDuplicateTagIds {
+ @ForyField(id = 10)
+ private String fieldA;
+
+ @ForyField(id = 10)
+ private String fieldB;
+
+ @ForyField(id = 20)
+ private int fieldC;
+ }
+
+ @Data
+ public static class ClassWithValidTagIds {
+ @ForyField(id = 10)
+ private String fieldA;
+
+ @ForyField(id = 20)
+ private String fieldB;
+
+ @ForyField(id = 30)
+ private int fieldC;
+ }
+
+ @Data
+ public static class ClassWithMixedFields {
+ @ForyField(id = 15)
+ private String annotatedField1;
+
+ private String noAnnotation;
+
+ @ForyField(id = 15) // Duplicate with annotatedField1
+ private int annotatedField2;
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithDuplicateTagIds() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> buildFieldsInfo(fory.getClassResolver(),
ClassWithDuplicateTagIds.class));
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithValidTagIds() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Should not throw any exception
+ List<ClassDef.FieldInfo> fieldsInfo =
+ buildFieldsInfo(fory.getClassResolver(), ClassWithValidTagIds.class);
+
+ Assert.assertEquals(fieldsInfo.size(), 3);
+ // Verify all fields have the correct tag IDs
+ for (ClassDef.FieldInfo fieldInfo : fieldsInfo) {
+ Assert.assertTrue(fieldInfo.hasFieldId());
+ }
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithMixedFields() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> buildFieldsInfo(fory.getClassResolver(),
ClassWithMixedFields.class));
+ }
}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/meta/ClassDefTest.java
b/java/fory-core/src/test/java/org/apache/fory/meta/ClassDefTest.java
index db45a58d7..25a57d0f5 100644
--- a/java/fory-core/src/test/java/org/apache/fory/meta/ClassDefTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/meta/ClassDefTest.java
@@ -33,11 +33,14 @@ import java.util.Map;
import java.util.TreeSet;
import org.apache.fory.Fory;
import org.apache.fory.ForyTestBase;
+import org.apache.fory.annotation.ForyField;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.resolver.ClassResolver;
import org.apache.fory.test.bean.Foo;
+import org.apache.fory.type.Descriptor;
+import org.testng.Assert;
import org.testng.annotations.Test;
public class ClassDefTest extends ForyTestBase {
@@ -188,4 +191,308 @@ public class ClassDefTest extends ForyTestBase {
assertTrue(classResolver.needToWriteRef(TypeRef.of(Foo.class, new
TypeExtMeta(true, true))));
assertFalse(classResolver.needToWriteRef(TypeRef.of(Foo.class, new
TypeExtMeta(true, false))));
}
+
+ // Test classes for duplicate tag ID validation
+ static class ClassWithDuplicateTagIds {
+ @ForyField(id = 1)
+ private String field1;
+
+ @ForyField(id = 1)
+ private String field2;
+
+ @ForyField(id = 2)
+ private int field3;
+ }
+
+ static class ClassWithDuplicateTagIdsMultiple {
+ @ForyField(id = 5)
+ private String field1;
+
+ @ForyField(id = 5)
+ private String field2;
+
+ @ForyField(id = 5)
+ private int field3;
+ }
+
+ static class ClassWithValidTagIds {
+ @ForyField(id = 1)
+ private String field1;
+
+ @ForyField(id = 2)
+ private String field2;
+
+ @ForyField(id = 3)
+ private int field3;
+ }
+
+ @Test
+ public void testDuplicateTagIdsThrowsException() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+ List<Field> fields =
ReflectionUtils.getFields(ClassWithDuplicateTagIds.class, true);
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClassDef.buildClassDef(
+ fory.getClassResolver(), ClassWithDuplicateTagIds.class,
fields));
+ }
+
+ @Test
+ public void testDuplicateTagIdsMultipleThrowsException() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+ List<Field> fields =
ReflectionUtils.getFields(ClassWithDuplicateTagIdsMultiple.class, true);
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClassDef.buildClassDef(
+ fory.getClassResolver(),
ClassWithDuplicateTagIdsMultiple.class, fields));
+ }
+
+ @Test
+ public void testValidTagIdsSucceeds() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+ List<Field> fields = ReflectionUtils.getFields(ClassWithValidTagIds.class,
true);
+
+ // Should not throw any exception
+ ClassDef classDef =
+ ClassDef.buildClassDef(fory.getClassResolver(),
ClassWithValidTagIds.class, fields);
+ assertEquals(classDef.getClassName(),
ClassWithValidTagIds.class.getName());
+ assertEquals(classDef.getFieldsInfo().size(), fields.size());
+ }
+
+ // Test classes for getDescriptors method
+ static class TargetClassWithDuplicateTagIds {
+ @ForyField(id = 100)
+ private String field1;
+
+ @ForyField(id = 100) // Duplicate tag ID
+ private String field2;
+
+ @ForyField(id = 200)
+ private int field3;
+ }
+
+ static class TargetClassWithValidTags {
+ @ForyField(id = 10)
+ private String taggedField1;
+
+ @ForyField(id = 20)
+ private int taggedField2;
+
+ private String normalField;
+ }
+
+ static class TargetClassWithMixedTags {
+ @ForyField(id = 50)
+ private String field1;
+
+ private String field2;
+
+ @ForyField(id = 60)
+ private int field3;
+ }
+
+ @Test
+ public void testGetDescriptorsWithDuplicateTagIds() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Build a ClassDef with valid fields (no duplicates in ClassDef itself)
+ List<Field> sourceFields =
ReflectionUtils.getFields(ClassWithValidTagIds.class, true);
+ ClassDef classDef =
+ ClassDef.buildClassDef(fory.getClassResolver(),
ClassWithValidTagIds.class, sourceFields);
+
+ // Try to get descriptors for a class that has duplicate tag IDs
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ classDef.getDescriptors(fory.getClassResolver(),
TargetClassWithDuplicateTagIds.class));
+ }
+
+ @Test
+ public void testGetDescriptorsWithValidTags() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Build a ClassDef with tagged fields
+ List<Field> sourceFields =
ReflectionUtils.getFields(TargetClassWithValidTags.class, true);
+ ClassDef classDef =
+ ClassDef.buildClassDef(
+ fory.getClassResolver(), TargetClassWithValidTags.class,
sourceFields);
+
+ // Get descriptors should succeed
+ List<Descriptor> descriptors =
+ classDef.getDescriptors(fory.getClassResolver(),
TargetClassWithValidTags.class);
+
+ assertEquals(descriptors.size(), 3);
+ }
+
+ @Test
+ public void testGetDescriptorsWithMixedTags() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Build a ClassDef with mixed tagged and non-tagged fields
+ List<Field> sourceFields =
ReflectionUtils.getFields(TargetClassWithMixedTags.class, true);
+ ClassDef classDef =
+ ClassDef.buildClassDef(
+ fory.getClassResolver(), TargetClassWithMixedTags.class,
sourceFields);
+
+ // Get descriptors should succeed
+ List<Descriptor> descriptors =
+ classDef.getDescriptors(fory.getClassResolver(),
TargetClassWithMixedTags.class);
+
+ assertEquals(descriptors.size(), 3);
+
+ // Verify that tagged fields are matched by tag, not by name
+ boolean foundField1 = false;
+ boolean foundField2 = false;
+ boolean foundField3 = false;
+
+ for (Descriptor desc : descriptors) {
+ if (desc.getName().equals("field1")) {
+ foundField1 = true;
+ } else if (desc.getName().equals("field2")) {
+ foundField2 = true;
+ } else if (desc.getName().equals("field3")) {
+ foundField3 = true;
+ }
+ }
+
+ assertTrue(foundField1);
+ assertTrue(foundField2);
+ assertTrue(foundField3);
+ }
+
+ static class SourceClassWithTags {
+ @ForyField(id = 100)
+ private String renamedField; // This will be matched by tag ID
+
+ @ForyField(id = 200)
+ private int anotherField;
+ }
+
+ static class TargetClassWithDifferentNames {
+ @ForyField(id = 100)
+ private String differentName; // Same tag ID as renamedField
+
+ @ForyField(id = 200)
+ private int alsoRenamed; // Same tag ID as anotherField
+ }
+
+ @Test
+ public void testGetDescriptorsMatchesByTagNotName() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Build a ClassDef from source class with specific tag IDs
+ List<Field> sourceFields =
ReflectionUtils.getFields(SourceClassWithTags.class, true);
+ ClassDef classDef =
+ ClassDef.buildClassDef(fory.getClassResolver(),
SourceClassWithTags.class, sourceFields);
+
+ // Get descriptors for target class with different field names but same
tag IDs
+ List<Descriptor> descriptors =
+ classDef.getDescriptors(fory.getClassResolver(),
TargetClassWithDifferentNames.class);
+
+ // Should match fields by tag ID, not by name
+ assertEquals(descriptors.size(), 2);
+
+ // Verify the descriptors were matched correctly (by tag, not name)
+ // When matched by tag, descriptors will have the target class field
information
+ for (Descriptor desc : descriptors) {
+ // The descriptor should have the field from the target class since it
was matched by tag
+ assertTrue(
+ desc.getName().equals("differentName") ||
desc.getName().equals("alsoRenamed"),
+ "Descriptor name should match target class field names when matched
by tag ID");
+ }
+ }
+
+ static class TargetClassWithZeroTagId {
+ @ForyField(id = 0)
+ private String field1;
+
+ @ForyField(id = 0) // Duplicate tag ID 0
+ private String field2;
+ }
+
+ @Test
+ public void testGetDescriptorsWithDuplicateZeroTagIds() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Build a ClassDef with some fields
+ List<Field> sourceFields =
ReflectionUtils.getFields(ClassWithValidTagIds.class, true);
+ ClassDef classDef =
+ ClassDef.buildClassDef(fory.getClassResolver(),
ClassWithValidTagIds.class, sourceFields);
+
+ // Try to get descriptors for a class that has duplicate tag ID 0
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> classDef.getDescriptors(fory.getClassResolver(),
TargetClassWithZeroTagId.class));
+ }
+
+ static class EmptyClass {
+ // No fields
+ }
+
+ @Test
+ public void testGetDescriptorsWithEmptyClass() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Build a ClassDef with no fields
+ List<Field> sourceFields = ReflectionUtils.getFields(EmptyClass.class,
true);
+ ClassDef classDef =
+ ClassDef.buildClassDef(fory.getClassResolver(), EmptyClass.class,
sourceFields);
+
+ // Get descriptors should succeed and return empty list
+ List<Descriptor> descriptors =
+ classDef.getDescriptors(fory.getClassResolver(), EmptyClass.class);
+
+ assertEquals(descriptors.size(), 0);
+ }
+
+ static class InheritedBaseClass {
+ @ForyField(id = 10)
+ private String baseField;
+ }
+
+ static class InheritedChildClass extends InheritedBaseClass {
+ @ForyField(id = 20)
+ private String childField;
+ }
+
+ static class InheritedChildWithDuplicateTag extends InheritedBaseClass {
+ @ForyField(id = 10) // Duplicate with baseField
+ private String childField;
+ }
+
+ @Test
+ public void testGetDescriptorsWithInheritance() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Build a ClassDef with inherited fields
+ List<Field> sourceFields =
ReflectionUtils.getFields(InheritedChildClass.class, true);
+ ClassDef classDef =
+ ClassDef.buildClassDef(fory.getClassResolver(),
InheritedChildClass.class, sourceFields);
+
+ // Get descriptors should succeed
+ List<Descriptor> descriptors =
+ classDef.getDescriptors(fory.getClassResolver(),
InheritedChildClass.class);
+
+ // Should have both base and child fields
+ assertEquals(descriptors.size(), 2);
+ }
+
+ @Test
+ public void testGetDescriptorsWithInheritedDuplicateTag() {
+ Fory fory = Fory.builder().withMetaShare(true).build();
+
+ // Build a ClassDef with some fields
+ List<Field> sourceFields =
ReflectionUtils.getFields(InheritedBaseClass.class, true);
+ ClassDef classDef =
+ ClassDef.buildClassDef(fory.getClassResolver(),
InheritedBaseClass.class, sourceFields);
+
+ // Try to get descriptors for a class that has duplicate tag ID across
inheritance
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ classDef.getDescriptors(fory.getClassResolver(),
InheritedChildWithDuplicateTag.class));
+ }
}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefEncoderTest.java
b/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefEncoderTest.java
new file mode 100644
index 000000000..e006935f0
--- /dev/null
+++ b/java/fory-core/src/test/java/org/apache/fory/meta/TypeDefEncoderTest.java
@@ -0,0 +1,369 @@
+/*
+ * 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 org.apache.fory.meta;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import lombok.Data;
+import org.apache.fory.Fory;
+import org.apache.fory.annotation.ForyField;
+import org.apache.fory.config.Language;
+import org.apache.fory.resolver.TypeResolver;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class TypeDefEncoderTest {
+
+ // Test data: Class with duplicate tag IDs (both set to 100)
+ @Data
+ public static class ClassWithDuplicateTagIds {
+ @ForyField(id = 100)
+ private String field1;
+
+ @ForyField(id = 100)
+ private String field2;
+
+ @ForyField(id = 200)
+ private int field3;
+ }
+
+ // Test data: Class with duplicate tag IDs set to 0
+ @Data
+ public static class ClassWithDuplicateTagIdsZero {
+ @ForyField(id = 0)
+ private String field1;
+
+ @ForyField(id = 0)
+ private int field2;
+ }
+
+ // Test data: Class with valid unique tag IDs
+ @Data
+ public static class ClassWithValidTagIds {
+ @ForyField(id = 100)
+ private String field1;
+
+ @ForyField(id = 200)
+ private String field2;
+
+ @ForyField(id = 300)
+ private int field3;
+ }
+
+ // Test data: Class with mixed annotations (with and without tags)
+ @Data
+ public static class ClassWithMixedAnnotations {
+ @ForyField(id = 50)
+ private String annotatedField1;
+
+ private String noAnnotation;
+
+ @ForyField(id = -1) // -1 means use field name
+ private String optOutField;
+
+ @ForyField(id = 60)
+ private int annotatedField2;
+ }
+
+ // Test data: Class with duplicate tag IDs in mixed scenario
+ @Data
+ public static class ClassWithMixedDuplicateTagIds {
+ @ForyField(id = 50)
+ private String annotatedField1;
+
+ private String noAnnotation;
+
+ @ForyField(id = -1) // -1 means use field name
+ private String optOutField;
+
+ @ForyField(id = 50) // Duplicate with annotatedField1
+ private int annotatedField2;
+ }
+
+ // Test data: Class with single field
+ @Data
+ public static class ClassWithSingleField {
+ @ForyField(id = 42)
+ private String field;
+ }
+
+ // Test data: Class with no annotations
+ @Data
+ public static class ClassWithNoAnnotations {
+ private String field1;
+ private int field2;
+ private double field3;
+ }
+
+ // Test data: Class with all fields using field names (tagId = -1)
+ @Data
+ public static class ClassWithAllFieldNames {
+ @ForyField(id = -1)
+ private String field1;
+
+ @ForyField(id = -1)
+ private int field2;
+
+ @ForyField(id = -1)
+ private double field3;
+ }
+
+ // Test data: Class with large tag IDs
+ @Data
+ public static class ClassWithLargeTagIds {
+ @ForyField(id = 32000)
+ private String field1;
+
+ @ForyField(id = 32767) // Max short value
+ private int field2;
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithDuplicateTagIds() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithDuplicateTagIds.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
+ Arrays.asList(
+ getField(ClassWithDuplicateTagIds.class, "field1"),
+ getField(ClassWithDuplicateTagIds.class, "field2"),
+ getField(ClassWithDuplicateTagIds.class, "field3"));
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> TypeDefEncoder.buildFieldsInfo(resolver,
ClassWithDuplicateTagIds.class, fields));
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithDuplicateTagIdsZero() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithDuplicateTagIdsZero.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
+ Arrays.asList(
+ getField(ClassWithDuplicateTagIdsZero.class, "field1"),
+ getField(ClassWithDuplicateTagIdsZero.class, "field2"));
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> TypeDefEncoder.buildFieldsInfo(resolver,
ClassWithDuplicateTagIdsZero.class, fields));
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithValidTagIds() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithValidTagIds.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
+ Arrays.asList(
+ getField(ClassWithValidTagIds.class, "field1"),
+ getField(ClassWithValidTagIds.class, "field2"),
+ getField(ClassWithValidTagIds.class, "field3"));
+
+ List<ClassDef.FieldInfo> fieldInfos =
+ TypeDefEncoder.buildFieldsInfo(resolver, ClassWithValidTagIds.class,
fields);
+
+ Assert.assertEquals(fieldInfos.size(), 3);
+
+ // Verify all fields have the correct tag IDs
+ Assert.assertTrue(fieldInfos.get(0).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(0).getFieldId(), (short) 100);
+ Assert.assertEquals(fieldInfos.get(0).getFieldName(), "field1");
+
+ Assert.assertTrue(fieldInfos.get(1).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(1).getFieldId(), (short) 200);
+ Assert.assertEquals(fieldInfos.get(1).getFieldName(), "field2");
+
+ Assert.assertTrue(fieldInfos.get(2).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(2).getFieldId(), (short) 300);
+ Assert.assertEquals(fieldInfos.get(2).getFieldName(), "field3");
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithMixedAnnotations() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithMixedAnnotations.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
+ Arrays.asList(
+ getField(ClassWithMixedAnnotations.class, "annotatedField1"),
+ getField(ClassWithMixedAnnotations.class, "noAnnotation"),
+ getField(ClassWithMixedAnnotations.class, "optOutField"),
+ getField(ClassWithMixedAnnotations.class, "annotatedField2"));
+
+ List<ClassDef.FieldInfo> fieldInfos =
+ TypeDefEncoder.buildFieldsInfo(resolver,
ClassWithMixedAnnotations.class, fields);
+
+ Assert.assertEquals(fieldInfos.size(), 4);
+
+ // annotatedField1 should have tag 50
+ Assert.assertTrue(fieldInfos.get(0).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(0).getFieldId(), (short) 50);
+ Assert.assertEquals(fieldInfos.get(0).getFieldName(), "annotatedField1");
+
+ // noAnnotation should not have a tag (uses field name)
+ Assert.assertFalse(fieldInfos.get(1).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(1).getFieldName(), "noAnnotation");
+
+ // optOutField with id=-1 should not have a tag (uses field name)
+ Assert.assertFalse(fieldInfos.get(2).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(2).getFieldName(), "optOutField");
+
+ // annotatedField2 should have tag 60
+ Assert.assertTrue(fieldInfos.get(3).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(3).getFieldId(), (short) 60);
+ Assert.assertEquals(fieldInfos.get(3).getFieldName(), "annotatedField2");
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithMixedDuplicateTagIds() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithMixedDuplicateTagIds.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
+ Arrays.asList(
+ getField(ClassWithMixedDuplicateTagIds.class, "annotatedField1"),
+ getField(ClassWithMixedDuplicateTagIds.class, "noAnnotation"),
+ getField(ClassWithMixedDuplicateTagIds.class, "optOutField"),
+ getField(ClassWithMixedDuplicateTagIds.class, "annotatedField2"));
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ TypeDefEncoder.buildFieldsInfo(resolver,
ClassWithMixedDuplicateTagIds.class, fields));
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithSingleField() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithSingleField.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
Collections.singletonList(getField(ClassWithSingleField.class, "field"));
+
+ List<ClassDef.FieldInfo> fieldInfos =
+ TypeDefEncoder.buildFieldsInfo(resolver, ClassWithSingleField.class,
fields);
+
+ Assert.assertEquals(fieldInfos.size(), 1);
+ Assert.assertTrue(fieldInfos.get(0).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(0).getFieldId(), (short) 42);
+ Assert.assertEquals(fieldInfos.get(0).getFieldName(), "field");
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithNoAnnotations() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithNoAnnotations.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
+ Arrays.asList(
+ getField(ClassWithNoAnnotations.class, "field1"),
+ getField(ClassWithNoAnnotations.class, "field2"),
+ getField(ClassWithNoAnnotations.class, "field3"));
+
+ List<ClassDef.FieldInfo> fieldInfos =
+ TypeDefEncoder.buildFieldsInfo(resolver, ClassWithNoAnnotations.class,
fields);
+
+ Assert.assertEquals(fieldInfos.size(), 3);
+
+ // All fields should not have tags (use field names)
+ for (ClassDef.FieldInfo fieldInfo : fieldInfos) {
+ Assert.assertFalse(fieldInfo.hasFieldId());
+ }
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithAllFieldNames() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithAllFieldNames.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
+ Arrays.asList(
+ getField(ClassWithAllFieldNames.class, "field1"),
+ getField(ClassWithAllFieldNames.class, "field2"),
+ getField(ClassWithAllFieldNames.class, "field3"));
+
+ List<ClassDef.FieldInfo> fieldInfos =
+ TypeDefEncoder.buildFieldsInfo(resolver, ClassWithAllFieldNames.class,
fields);
+
+ Assert.assertEquals(fieldInfos.size(), 3);
+
+ // All fields with id=-1 should not have tags (use field names)
+ for (ClassDef.FieldInfo fieldInfo : fieldInfos) {
+ Assert.assertFalse(fieldInfo.hasFieldId());
+ }
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithLargeTagIds() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithLargeTagIds.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields =
+ Arrays.asList(
+ getField(ClassWithLargeTagIds.class, "field1"),
+ getField(ClassWithLargeTagIds.class, "field2"));
+
+ List<ClassDef.FieldInfo> fieldInfos =
+ TypeDefEncoder.buildFieldsInfo(resolver, ClassWithLargeTagIds.class,
fields);
+
+ Assert.assertEquals(fieldInfos.size(), 2);
+
+ Assert.assertTrue(fieldInfos.get(0).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(0).getFieldId(), (short) 32000);
+
+ Assert.assertTrue(fieldInfos.get(1).hasFieldId());
+ Assert.assertEquals(fieldInfos.get(1).getFieldId(), (short) 32767);
+ }
+
+ @Test
+ public void testBuildFieldsInfoWithEmptyFieldList() {
+ Fory fory =
Fory.builder().withLanguage(Language.XLANG).withMetaShare(true).build();
+ fory.register(ClassWithValidTagIds.class);
+ TypeResolver resolver = fory.getXtypeResolver();
+
+ List<Field> fields = Collections.emptyList();
+
+ List<ClassDef.FieldInfo> fieldInfos =
+ TypeDefEncoder.buildFieldsInfo(resolver, ClassWithValidTagIds.class,
fields);
+
+ Assert.assertEquals(fieldInfos.size(), 0);
+ }
+
+ /** Helper method to get a field from a class by name. */
+ private Field getField(Class<?> clazz, String fieldName) {
+ try {
+ return clazz.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(
+ "Field not found: " + fieldName + " in class " + clazz.getName(), e);
+ }
+ }
+}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/type/DescriptorGrouperTest.java
b/java/fory-core/src/test/java/org/apache/fory/type/DescriptorGrouperTest.java
index 99277da4c..015728887 100644
---
a/java/fory-core/src/test/java/org/apache/fory/type/DescriptorGrouperTest.java
+++
b/java/fory-core/src/test/java/org/apache/fory/type/DescriptorGrouperTest.java
@@ -41,13 +41,15 @@ public class DescriptorGrouperTest {
List<Descriptor> descriptors = new ArrayList<>();
int index = 0;
for (Class<?> aClass : Primitives.allPrimitiveTypes()) {
- descriptors.add(new Descriptor(TypeRef.of(aClass), "f" + index++, -1,
"TestClass"));
+ descriptors.add(new Descriptor(TypeRef.of(aClass), "f" + index++, -1,
"TestClass", false));
}
for (Class<?> t : Primitives.allWrapperTypes()) {
- descriptors.add(new Descriptor(TypeRef.of(t), "f" + index++, -1,
"TestClass"));
+ descriptors.add(new Descriptor(TypeRef.of(t), "f" + index++, -1,
"TestClass", false));
}
- descriptors.add(new Descriptor(TypeRef.of(String.class), "f" + index++,
-1, "TestClass"));
- descriptors.add(new Descriptor(TypeRef.of(Object.class), "f" + index++,
-1, "TestClass"));
+ descriptors.add(
+ new Descriptor(TypeRef.of(String.class), "f" + index++, -1,
"TestClass", false));
+ descriptors.add(
+ new Descriptor(TypeRef.of(Object.class), "f" + index++, -1,
"TestClass", false));
Collections.shuffle(descriptors, new Random(17));
return descriptors;
}
@@ -88,7 +90,7 @@ public class DescriptorGrouperTest {
List<Descriptor> descriptors = new ArrayList<>();
int index = 0;
for (Class<?> aClass : Primitives.allPrimitiveTypes()) {
- descriptors.add(new Descriptor(TypeRef.of(aClass), "f" + index++, -1,
"TestClass"));
+ descriptors.add(new Descriptor(TypeRef.of(aClass), "f" + index++, -1,
"TestClass", false));
}
Collections.shuffle(descriptors, new Random(7));
descriptors.sort(DescriptorGrouper.getPrimitiveComparator(false, false));
@@ -113,7 +115,7 @@ public class DescriptorGrouperTest {
List<Descriptor> descriptors = new ArrayList<>();
int index = 0;
for (Class<?> aClass : Primitives.allPrimitiveTypes()) {
- descriptors.add(new Descriptor(TypeRef.of(aClass), "f" + index++, -1,
"TestClass"));
+ descriptors.add(new Descriptor(TypeRef.of(aClass), "f" + index++, -1,
"TestClass", false));
}
Collections.shuffle(descriptors, new Random(7));
descriptors.sort(DescriptorGrouper.getPrimitiveComparator(true, true));
@@ -137,17 +139,23 @@ public class DescriptorGrouperTest {
public void testGrouper() {
List<Descriptor> descriptors = createDescriptors();
int index = 0;
- descriptors.add(new Descriptor(TypeRef.of(Object.class), "c" + index++,
-1, "TestClass"));
- descriptors.add(new Descriptor(TypeRef.of(Date.class), "c" + index++, -1,
"TestClass"));
- descriptors.add(new Descriptor(TypeRef.of(Instant.class), "c" + index++,
-1, "TestClass"));
- descriptors.add(new Descriptor(TypeRef.of(Instant.class), "c" + index++,
-1, "TestClass"));
- descriptors.add(new Descriptor(new TypeRef<List<String>>() {}, "c" +
index++, -1, "TestClass"));
descriptors.add(
- new Descriptor(new TypeRef<List<Integer>>() {}, "c" + index++, -1,
"TestClass"));
+ new Descriptor(TypeRef.of(Object.class), "c" + index++, -1,
"TestClass", false));
+ descriptors.add(new Descriptor(TypeRef.of(Date.class), "c" + index++, -1,
"TestClass", false));
+ descriptors.add(
+ new Descriptor(TypeRef.of(Instant.class), "c" + index++, -1,
"TestClass", false));
+ descriptors.add(
+ new Descriptor(TypeRef.of(Instant.class), "c" + index++, -1,
"TestClass", false));
+ descriptors.add(
+ new Descriptor(new TypeRef<List<String>>() {}, "c" + index++, -1,
"TestClass", false));
+ descriptors.add(
+ new Descriptor(new TypeRef<List<Integer>>() {}, "c" + index++, -1,
"TestClass", false));
descriptors.add(
- new Descriptor(new TypeRef<Map<String, Integer>>() {}, "c" + index++,
-1, "TestClass"));
+ new Descriptor(
+ new TypeRef<Map<String, Integer>>() {}, "c" + index++, -1,
"TestClass", false));
descriptors.add(
- new Descriptor(new TypeRef<Map<String, String>>() {}, "c" + index++,
-1, "TestClass"));
+ new Descriptor(
+ new TypeRef<Map<String, String>>() {}, "c" + index++, -1,
"TestClass", false));
DescriptorGrouper grouper =
DescriptorGrouper.createDescriptorGrouper(
ReflectionUtils::isMonomorphic,
diff --git
a/java/fory-core/src/test/java/org/apache/fory/type/DescriptorTest.java
b/java/fory-core/src/test/java/org/apache/fory/type/DescriptorTest.java
index a591b4b77..99603d413 100644
--- a/java/fory-core/src/test/java/org/apache/fory/type/DescriptorTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/type/DescriptorTest.java
@@ -97,7 +97,7 @@ public class DescriptorTest {
@Test
public void testDescriptorBuilder() {
- Descriptor descriptor = new Descriptor(TypeRef.of(A.class), "c", -1,
"TestClass");
+ Descriptor descriptor = new Descriptor(TypeRef.of(A.class), "c", -1,
"TestClass", true);
// test copyBuilder
Descriptor descriptor1 = descriptor.copyBuilder().build();
Assert.assertEquals(descriptor.getTypeRef(), descriptor1.getTypeRef());
diff --git
a/java/fory-format/src/test/java/org/apache/fory/format/encoder/ImplementInterfaceTest.java
b/java/fory-format/src/test/java/org/apache/fory/format/encoder/ImplementInterfaceTest.java
index 62661f18b..7f532da5c 100644
---
a/java/fory-format/src/test/java/org/apache/fory/format/encoder/ImplementInterfaceTest.java
+++
b/java/fory-format/src/test/java/org/apache/fory/format/encoder/ImplementInterfaceTest.java
@@ -56,7 +56,7 @@ public class ImplementInterfaceTest {
}
public interface NestedType {
- @ForyField(nullable = false)
+ @ForyField(id = 0, nullable = false)
String getF3();
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]