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 a6aead8ee feat(scala): support scala object with default values (#2412)
a6aead8ee is described below

commit a6aead8eeed8ba5f1dd2e778dffee9e6d52abfee
Author: Shawn Yang <shawn.ck.y...@gmail.com>
AuthorDate: Tue Jul 15 01:35:40 2025 +0800

    feat(scala): support scala object with default values (#2412)
    
    ## What does this PR do?
    
    Support scala case default values. The implementation consists of:
    
    1. **`ScalaCaseClassUtils`**: Utility class for detecting Scala case
    classes and retrieving default values from companion object methods.
    
    2. **`MetaSharedSerializer`**: Modified to cache default value field
    information and apply default values to missing fields just before
    returning the deserialized object.
    
    3. **`ScalaDefaultValueField`**: Internal field info class that caches
    field accessors and default values for efficient application during
    deserialization.
    
    4. **Cached Processing**: During serializer initialization, default
    values are discovered and cached. During deserialization, cached default
    values are applied to any null fields that have corresponding default
    parameters.
    
    This feature enhances Fury's compatibility with Scala case classes and
    provides better support for evolving schemas with default parameters.
    
    ## Related issues
    
    Closes #1683
    
    ## Does this PR introduce any user-facing change?
    
    <!--
    If any user-facing interface changes, please [open an
    issue](https://github.com/apache/fory/issues/new/choose) describing the
    need to do so and update the document if necessary.
    -->
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    <!--
    When the PR has an impact on performance (if you don't know whether the
    PR will have an impact on performance, you can submit the PR first, and
    if it will have impact on performance, the code reviewer will explain
    it), be sure to attach a benchmark data here.
    -->
---
 docs/guide/scala_guide.md                          | 149 ++++++++
 .../src/main/java/org/apache/fory/Fory.java        |  12 +-
 .../fory/builder/MetaSharedCodecBuilder.java       |  57 +++
 .../org/apache/fory/collection/Collections.java    |  23 ++
 .../fory/serializer/MetaSharedSerializer.java      |  30 +-
 .../main/java/org/apache/fory/type/ScalaTypes.java |  12 +
 .../apache/fory/util/ScalaDefaultValueUtils.java   | 385 +++++++++++++++++++++
 .../serializer/scala/ScalaDefaultValueTest.scala   | 199 +++++++++++
 .../fory/util/ScalaDefaultValueUtilsTest.scala     | 383 ++++++++++++++++++++
 9 files changed, 1248 insertions(+), 2 deletions(-)

diff --git a/docs/guide/scala_guide.md b/docs/guide/scala_guide.md
index 83b0d4d7c..c0ca64963 100644
--- a/docs/guide/scala_guide.md
+++ b/docs/guide/scala_guide.md
@@ -162,3 +162,152 @@ println(fory.deserialize(fory.serialize(opt)))
 val opt1: Option[Long] = None
 println(fory.deserialize(fory.serialize(opt1)))
 ```
+
+## Scala Class Default Values Support
+
+Fory supports Scala class default values during deserialization when using 
compatible mode. This feature enables forward/backward compatibility when case 
classes or regular Scala classes have default parameters.
+
+### Overview
+
+When a Scala class has default parameters, the Scala compiler generates 
methods in the companion object (for case classes) or in the class itself (for 
regular Scala classes) like `apply$default$1`, `apply$default$2`, etc. that 
return the default values. Fory can detect these methods and use them when 
deserializing objects where certain fields are missing from the serialized data.
+
+### Supported Class Types
+
+Fory supports default values for:
+
+- **Case classes** with default parameters
+- **Regular Scala classes** with default parameters in their primary 
constructor
+- **Nested case classes** with default parameters
+- **Deeply nested case classes** with default parameters
+
+### How It Works
+
+1. **Detection**: Fory detects if a class is a Scala class by checking for the 
presence of default value methods (`apply$default$N` or `$default$N`).
+
+2. **Default Value Discovery**:
+   - For case classes: Fory scans the companion object for methods named 
`apply$default$1`, `apply$default$2`, etc.
+   - For regular Scala classes: Fory scans the class itself for methods named 
`$default$1`, `$default$2`, etc.
+
+3. **Field Mapping**: During deserialization, Fory identifies fields that 
exist in the target class but are missing from the serialized data.
+
+4. **Value Application**: After reading all available fields from the 
serialized data, Fory applies default values to any missing fields using direct 
field access for optimal performance.
+
+### Usage
+
+This feature is automatically enabled when:
+
+- Compatible mode is enabled (`withCompatibleMode(CompatibleMode.COMPATIBLE)`)
+- The target class is detected as a Scala class with default values
+- A field is missing from the serialized data but exists in the target class
+
+No additional configuration is required.
+
+### Examples
+
+#### Case Class with Default Values
+
+```scala
+// Class WITHOUT default values (for serialization)
+case class PersonNoDefaults(name: String)
+
+// Class WITH default values (for deserialization)
+case class PersonWithDefaults(name: String, age: Int = 25, city: String = 
"Unknown")
+
+val fory = Fory.builder()
+  .withCompatibleMode(CompatibleMode.COMPATIBLE)
+  .withScalaOptimizationEnabled(true)
+  .build()
+
+// Serialize using class without default values
+val original = PersonNoDefaults("John")
+val serialized = fory.serialize(original)
+
+// Deserialize into class with default values - missing fields will use 
defaults
+val deserialized = fory.deserialize(serialized, classOf[PersonWithDefaults])
+// deserialized.name will be "John"
+// deserialized.age will be 25 (default)
+// deserialized.city will be "Unknown" (default)
+```
+
+#### Regular Scala Class with Default Values
+
+```scala
+// Class WITHOUT default values (for serialization)
+class EmployeeNoDefaults(val name: String)
+
+// Class WITH default values (for deserialization)
+class EmployeeWithDefaults(val name: String, val age: Int = 30, val 
department: String = "Engineering")
+
+val fory = Fory.builder()
+  .withCompatibleMode(CompatibleMode.COMPATIBLE)
+  .withScalaOptimizationEnabled(true)
+  .build()
+
+// Serialize using class without default values
+val original = new EmployeeNoDefaults("Jane")
+val serialized = fory.serialize(original)
+
+// Deserialize into class with default values - missing fields will use 
defaults
+val deserialized = fory.deserialize(serialized, classOf[EmployeeWithDefaults])
+// deserialized.name will be "Jane"
+// deserialized.age will be 30 (default)
+// deserialized.department will be "Engineering" (default)
+```
+
+#### Complex Default Values
+
+```scala
+// Class WITHOUT default values (for serialization)
+case class ConfigurationNoDefaults(name: String)
+
+// Class WITH default values (for deserialization)
+case class ConfigurationWithDefaults(
+  name: String,
+  settings: Map[String, String] = Map("default" -> "value"),
+  tags: List[String] = List("default"),
+  enabled: Boolean = true
+)
+
+val fory = Fory.builder()
+  .withCompatibleMode(CompatibleMode.COMPATIBLE)
+  .withScalaOptimizationEnabled(true)
+  .build()
+
+// Serialize using class without default values
+val original = ConfigurationNoDefaults("myConfig")
+val serialized = fory.serialize(original)
+
+// Deserialize into class with default values - missing fields will use 
defaults
+val deserialized = fory.deserialize(serialized, 
classOf[ConfigurationWithDefaults])
+// deserialized.name will be "myConfig"
+// deserialized.settings will be Map("default" -> "value")
+// deserialized.tags will be List("default")
+// deserialized.enabled will be true
+```
+
+#### Nested Case Classes
+
+```scala
+object NestedClasses {
+  // Class WITHOUT default values (for serialization)
+  case class SimplePerson(name: String)
+
+  // Class WITH default values (for deserialization)
+  case class Address(street: String, city: String = "DefaultCity")
+  case class PersonWithDefaults(name: String, address: Address = 
Address("DefaultStreet"))
+}
+
+val fory = Fory.builder()
+  .withCompatibleMode(CompatibleMode.COMPATIBLE)
+  .withScalaOptimizationEnabled(true)
+  .build()
+
+// Serialize using class without default values
+val original = NestedClasses.SimplePerson("Alice")
+val serialized = fory.serialize(original)
+
+// Deserialize into class with default values - missing address field will use 
default
+val deserialized = fory.deserialize(serialized, 
classOf[NestedClasses.PersonWithDefaults])
+// deserialized.name will be "Alice"
+// deserialized.address will be Address("DefaultStreet", "DefaultCity")
+```
diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java 
b/java/fory-core/src/main/java/org/apache/fory/Fory.java
index a2840ef5d..99eed4bbf 100644
--- a/java/fory-core/src/main/java/org/apache/fory/Fory.java
+++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java
@@ -796,9 +796,19 @@ public final class Fory implements BaseFory {
   @SuppressWarnings("unchecked")
   @Override
   public <T> T deserialize(byte[] bytes, Class<T> type) {
+    MemoryBuffer buffer = MemoryUtils.wrap(bytes);
+    if (!crossLanguage && shareMeta) {
+      byte bitmap = buffer.readByte();
+      if ((bitmap & isNilFlag) == isNilFlag) {
+        return null;
+      }
+      boolean peerOutOfBandEnabled = (bitmap & isOutOfBandFlag) == 
isOutOfBandFlag;
+      assert !peerOutOfBandEnabled : "Out of band buffers not passed in when 
deserializing";
+      return deserializeJavaObject(buffer, type);
+    }
     generics.pushGenericType(classResolver.buildGenericType(type));
     try {
-      return (T) deserialize(MemoryUtils.wrap(bytes), null);
+      return (T) deserialize(buffer, null);
     } finally {
       generics.popGenericType();
     }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java
 
b/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java
index 099b0081d..649a0fc15 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java
@@ -20,7 +20,10 @@
 package org.apache.fory.builder;
 
 import static 
org.apache.fory.builder.Generated.GeneratedMetaSharedSerializer.SERIALIZER_FIELD_NAME;
+import static org.apache.fory.type.TypeUtils.OBJECT_TYPE;
+import static org.apache.fory.type.TypeUtils.STRING_TYPE;
 
+import java.lang.reflect.Member;
 import java.util.Collection;
 import java.util.Map;
 import java.util.SortedMap;
@@ -30,6 +33,7 @@ import 
org.apache.fory.builder.Generated.GeneratedMetaSharedSerializer;
 import org.apache.fory.codegen.CodeGenerator;
 import org.apache.fory.codegen.Expression;
 import org.apache.fory.codegen.Expression.Literal;
+import org.apache.fory.codegen.Expression.StaticInvoke;
 import org.apache.fory.config.CompatibleMode;
 import org.apache.fory.config.ForyBuilder;
 import org.apache.fory.memory.MemoryBuffer;
@@ -45,6 +49,7 @@ import org.apache.fory.type.DescriptorGrouper;
 import org.apache.fory.util.ExceptionUtils;
 import org.apache.fory.util.GraalvmSupport;
 import org.apache.fory.util.Preconditions;
+import org.apache.fory.util.ScalaDefaultValueUtils;
 import org.apache.fory.util.StringUtils;
 import org.apache.fory.util.record.RecordComponent;
 import org.apache.fory.util.record.RecordUtils;
@@ -67,6 +72,7 @@ import org.apache.fory.util.record.RecordUtils;
  */
 public class MetaSharedCodecBuilder extends ObjectCodecBuilder {
   private final ClassDef classDef;
+  private final ScalaDefaultValueUtils.ScalaDefaultValueField[] 
scalaDefaultValueFields;
 
   public MetaSharedCodecBuilder(TypeRef<?> beanType, Fory fory, ClassDef 
classDef) {
     super(beanType, fory, GeneratedMetaSharedSerializer.class);
@@ -84,6 +90,15 @@ public class MetaSharedCodecBuilder extends 
ObjectCodecBuilder {
     DescriptorGrouper grouper = 
fory.getClassResolver().createDescriptorGrouper(descriptors, false);
     objectCodecOptimizer =
         new ObjectCodecOptimizer(beanClass, grouper, 
!fory.isBasicTypesRefIgnored(), ctx);
+
+    if (fory.getConfig().isScalaOptimizationEnabled()) {
+      // Check if this is a Scala case class and build default value fields
+      this.scalaDefaultValueFields =
+          ScalaDefaultValueUtils.buildScalaDefaultValueFields(
+              fory, beanClass, grouper.getSortedDescriptors());
+    } else {
+      this.scalaDefaultValueFields = new 
ScalaDefaultValueUtils.ScalaDefaultValueField[0];
+    }
   }
 
   // Must be static to be shared across the whole process life.
@@ -140,6 +155,7 @@ public class MetaSharedCodecBuilder extends 
ObjectCodecBuilder {
   protected void addCommonImports() {
     super.addCommonImports();
     ctx.addImport(GeneratedMetaSharedSerializer.class);
+    ctx.addImport(ScalaDefaultValueUtils.class);
   }
 
   // Invoked by JIT.
@@ -196,4 +212,45 @@ public class MetaSharedCodecBuilder extends 
ObjectCodecBuilder {
     }
     return super.setFieldValue(bean, descriptor, value);
   }
+
+  @Override
+  protected Expression newBean() {
+    Expression bean = super.newBean();
+    if (scalaDefaultValueFields.length == 0) {
+      return bean;
+    }
+
+    Expression.ListExpression setDefaultsExpr = new 
Expression.ListExpression();
+    setDefaultsExpr.add(bean);
+    Map<Member, Descriptor> descriptors = 
Descriptor.getAllDescriptorsMap(beanClass);
+    for (ScalaDefaultValueUtils.ScalaDefaultValueField defaultField : 
scalaDefaultValueFields) {
+      Object defaultValue = defaultField.getDefaultValue();
+      Member member = defaultField.getFieldAccessor().getField();
+      Descriptor descriptor = descriptors.get(member);
+      TypeRef<?> typeRef = descriptor.getTypeRef();
+      Expression defaultValueExpr;
+      if (typeRef.unwrap().isPrimitive() || typeRef.equals(STRING_TYPE)) {
+        defaultValueExpr = new Literal(defaultValue, typeRef);
+      } else {
+        defaultValueExpr =
+            getOrCreateField(
+                true,
+                typeRef.getRawType(),
+                member.getName(),
+                () -> {
+                  Expression expr =
+                      new StaticInvoke(
+                          ScalaDefaultValueUtils.class,
+                          "getDefaultValue",
+                          OBJECT_TYPE,
+                          staticBeanClassExpr(),
+                          Literal.ofString(member.getName()));
+                  return new Expression.Cast(expr, typeRef);
+                });
+      }
+      setDefaultsExpr.add(super.setFieldValue(bean, descriptor, 
defaultValueExpr));
+    }
+    setDefaultsExpr.add(bean);
+    return setDefaultsExpr;
+  }
 }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/collection/Collections.java 
b/java/fory-core/src/main/java/org/apache/fory/collection/Collections.java
index 6a8475d85..347af20a7 100644
--- a/java/fory-core/src/main/java/org/apache/fory/collection/Collections.java
+++ b/java/fory-core/src/main/java/org/apache/fory/collection/Collections.java
@@ -19,6 +19,8 @@
 
 package org.apache.fory.collection;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.MapMaker;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -197,4 +199,25 @@ public class Collections {
       return new MapMaker().weakKeys().makeMap();
     }
   }
+
+  /**
+   * Create a cache with weak keys and soft values.
+   *
+   * <p>when in graalvm, the cache is a concurrent hash map. when in jvm, the 
cache is a weak hash
+   * map.
+   *
+   * @param concurrencyLevel the concurrency level
+   * @return the cache
+   */
+  public static <T> Cache<Class<?>, T> newClassKeySoftCache(int 
concurrencyLevel) {
+    if (GraalvmSupport.isGraalBuildtime()) {
+      return 
CacheBuilder.newBuilder().concurrencyLevel(concurrencyLevel).build();
+    } else {
+      return CacheBuilder.newBuilder()
+          .weakKeys()
+          .softValues()
+          .concurrencyLevel(concurrencyLevel)
+          .build();
+    }
+  }
 }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java
index 4d49e2578..245ef530f 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java
@@ -41,6 +41,7 @@ import org.apache.fory.type.Descriptor;
 import org.apache.fory.type.DescriptorGrouper;
 import org.apache.fory.type.Generics;
 import org.apache.fory.util.Preconditions;
+import org.apache.fory.util.ScalaDefaultValueUtils;
 import org.apache.fory.util.record.RecordInfo;
 import org.apache.fory.util.record.RecordUtils;
 
@@ -77,6 +78,8 @@ public class MetaSharedSerializer<T> extends 
AbstractObjectSerializer<T> {
   private Serializer<T> serializer;
   private final ClassInfoHolder classInfoHolder;
   private final SerializationBinding binding;
+  private final boolean hasScalaDefaultValues;
+  private final ScalaDefaultValueUtils.ScalaDefaultValueField[] 
scalaDefaultValueFields;
 
   public MetaSharedSerializer(Fory fory, Class<T> type, ClassDef classDef) {
     super(fory, type);
@@ -111,6 +114,15 @@ public class MetaSharedSerializer<T> extends 
AbstractObjectSerializer<T> {
       recordInfo = null;
     }
     binding = SerializationBinding.createBinding(fory);
+    if (fory.getConfig().isScalaOptimizationEnabled()) {
+      hasScalaDefaultValues = 
ScalaDefaultValueUtils.hasScalaDefaultValues(type);
+      scalaDefaultValueFields =
+          ScalaDefaultValueUtils.buildScalaDefaultValueFields(
+              fory, type, descriptorGrouper.getSortedDescriptors());
+    } else {
+      hasScalaDefaultValues = false;
+      scalaDefaultValueFields = new 
ScalaDefaultValueUtils.ScalaDefaultValueField[0];
+    }
   }
 
   @Override
@@ -134,6 +146,7 @@ public class MetaSharedSerializer<T> extends 
AbstractObjectSerializer<T> {
           new Object[finalFields.length + otherFields.length + 
containerFields.length];
       readFields(buffer, fieldValues);
       fieldValues = RecordUtils.remapping(recordInfo, fieldValues);
+
       try {
         T t = (T) constructor.invokeWithArguments(fieldValues);
         Arrays.fill(recordInfo.getRecordComponents(), null);
@@ -142,7 +155,7 @@ public class MetaSharedSerializer<T> extends 
AbstractObjectSerializer<T> {
         Platform.throwException(e);
       }
     }
-    T obj = newBean();
+    T obj = newInstance();
     Fory fory = this.fory;
     RefResolver refResolver = this.refResolver;
     ClassResolver classResolver = this.classResolver;
@@ -171,6 +184,7 @@ public class MetaSharedSerializer<T> extends 
AbstractObjectSerializer<T> {
           fieldAccessor.putObject(obj, fieldValue);
         }
       } else {
+        // Skip the field value from buffer since it doesn't exist in current 
class
         if (skipPrimitiveFieldValueFailed(fory, fieldInfo.classId, buffer)) {
           if (fieldInfo.classInfo == null) {
             // TODO(chaokunyang) support registered serializer in peer with 
ref tracking disabled.
@@ -198,9 +212,22 @@ public class MetaSharedSerializer<T> extends 
AbstractObjectSerializer<T> {
         fieldAccessor.putObject(obj, fieldValue);
       }
     }
+
+    // Set default values for missing fields in Scala case classes
+    if (hasScalaDefaultValues) {
+      ScalaDefaultValueUtils.setScalaDefaultValues(obj, 
scalaDefaultValueFields);
+    }
+
     return obj;
   }
 
+  private T newInstance() {
+    if (!hasScalaDefaultValues) {
+      return newBean();
+    }
+    return Platform.newInstance(type);
+  }
+
   @Override
   public T xread(MemoryBuffer buffer) {
     return read(buffer);
@@ -231,6 +258,7 @@ public class MetaSharedSerializer<T> extends 
AbstractObjectSerializer<T> {
           fields[counter++] = fieldValue;
         }
       } else {
+        // Skip the field value from buffer since it doesn't exist in current 
class
         if (skipPrimitiveFieldValueFailed(fory, fieldInfo.classId, buffer)) {
           if (fieldInfo.classInfo == null) {
             // TODO(chaokunyang) support registered serializer in peer with 
ref tracking disabled.
diff --git a/java/fory-core/src/main/java/org/apache/fory/type/ScalaTypes.java 
b/java/fory-core/src/main/java/org/apache/fory/type/ScalaTypes.java
index 84dff043b..4c32c1b93 100644
--- a/java/fory-core/src/main/java/org/apache/fory/type/ScalaTypes.java
+++ b/java/fory-core/src/main/java/org/apache/fory/type/ScalaTypes.java
@@ -34,6 +34,7 @@ public class ScalaTypes {
   private static volatile Class<?> SCALA_ITERABLE_TYPE;
   private static volatile java.lang.reflect.Type SCALA_ITERATOR_RETURN_TYPE;
   private static volatile java.lang.reflect.Type SCALA_NEXT_RETURN_TYPE;
+  private static volatile Class<?> SCALA_PRODUCT_TYPE;
 
   public static Class<?> getScalaMapType() {
     if (SCALA_MAP_TYPE == null) {
@@ -103,4 +104,15 @@ public class ScalaTypes {
     Type[] types = type.getActualTypeArguments();
     return Tuple2.of(TypeRef.of(types[0]), TypeRef.of(types[1]));
   }
+
+  public static Class<?> getScalaProductType() {
+    if (SCALA_PRODUCT_TYPE == null) {
+      SCALA_PRODUCT_TYPE = ReflectionUtils.loadClass("scala.Product");
+    }
+    return SCALA_PRODUCT_TYPE;
+  }
+
+  public static boolean isScalaProductType(Class<?> cls) {
+    return getScalaProductType().isAssignableFrom(cls);
+  }
 }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/util/ScalaDefaultValueUtils.java 
b/java/fory-core/src/main/java/org/apache/fory/util/ScalaDefaultValueUtils.java
new file mode 100644
index 000000000..bff06ab97
--- /dev/null
+++ 
b/java/fory-core/src/main/java/org/apache/fory/util/ScalaDefaultValueUtils.java
@@ -0,0 +1,385 @@
+/*
+ * 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.util;
+
+import com.google.common.cache.Cache;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.fory.Fory;
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.collection.Collections;
+import org.apache.fory.logging.Logger;
+import org.apache.fory.logging.LoggerFactory;
+import org.apache.fory.memory.Platform;
+import org.apache.fory.reflect.FieldAccessor;
+import org.apache.fory.resolver.ClassResolver;
+import org.apache.fory.type.ScalaTypes;
+import org.apache.fory.type.TypeUtils;
+import org.apache.fory.util.unsafe._JDKAccess;
+
+/**
+ * Utility class for detecting Scala classes with default values and their 
default value methods.
+ *
+ * <p>Scala classes (including case classes) with default parameters generate 
companion objects with
+ * methods like `apply$default$1`, `apply$default$2`, etc. that return the 
default values.
+ */
+@Internal
+public class ScalaDefaultValueUtils {
+  private static final Logger LOG = 
LoggerFactory.getLogger(ScalaDefaultValueUtils.class);
+
+  private static final Cache<Class<?>, Map<Integer, Object>> 
cachedCtrDefaultValues =
+      Collections.newClassKeySoftCache(32);
+  private static final Cache<Class<?>, ScalaDefaultValueField[]> 
defaultValueFieldsCache =
+      Collections.newClassKeySoftCache(32);
+  private static final Cache<Class<?>, Map<String, Object>> 
allDefaultValuesCache =
+      Collections.newClassKeySoftCache(32);
+
+  /** Field info for Scala case class fields with default values. */
+  public static final class ScalaDefaultValueField {
+    private final Object defaultValue;
+    private final String fieldName;
+    private final FieldAccessor fieldAccessor;
+    private final short classId;
+
+    private ScalaDefaultValueField(
+        String fieldName, Object defaultValue, FieldAccessor fieldAccessor, 
short classId) {
+      this.fieldName = fieldName;
+      this.defaultValue = defaultValue;
+      this.fieldAccessor = fieldAccessor;
+      this.classId = classId;
+    }
+
+    public Object getDefaultValue() {
+      return defaultValue;
+    }
+
+    public String getFieldName() {
+      return fieldName;
+    }
+
+    public FieldAccessor getFieldAccessor() {
+      return fieldAccessor;
+    }
+
+    public short getClassId() {
+      return classId;
+    }
+  }
+
+  public static boolean hasScalaDefaultValues(Class<?> cls) {
+    return getAllDefaultValues(cls).size() > 0;
+  }
+
+  /**
+   * Builds Scala default value fields for the given class. Only includes 
fields that are not
+   * present in the serialized data.
+   *
+   * @param fory the Fory instance
+   * @param type the class type
+   * @param descriptors list of descriptors that are present in the serialized 
data
+   * @return array of ScalaDefaultValueField objects
+   */
+  public static ScalaDefaultValueField[] buildScalaDefaultValueFields(
+      Fory fory, Class<?> type, 
java.util.List<org.apache.fory.type.Descriptor> descriptors) {
+    ScalaDefaultValueField[] defaultFieldsArray = 
defaultValueFieldsCache.getIfPresent(type);
+    if (defaultFieldsArray != null) {
+      return defaultFieldsArray;
+    }
+    try {
+      // Extract field names from descriptors
+      java.util.Set<String> serializedFieldNames = new java.util.HashSet<>();
+      for (org.apache.fory.type.Descriptor descriptor : descriptors) {
+        java.lang.reflect.Field field = descriptor.getField();
+        if (field != null) {
+          serializedFieldNames.add(field.getName());
+        }
+      }
+      java.lang.reflect.Field[] allFields = type.getDeclaredFields();
+      List<ScalaDefaultValueField> defaultFields = new ArrayList<>();
+      Map<String, Object> allDefaults = getAllDefaultValues(type);
+      for (java.lang.reflect.Field field : allFields) {
+        // Only include fields that are not in the serialized data
+        if (!serializedFieldNames.contains(field.getName())) {
+          String fieldName = field.getName();
+          Object defaultValue = allDefaults.get(fieldName);
+
+          if (defaultValue != null
+              && 
TypeUtils.wrap(field.getType()).isAssignableFrom(defaultValue.getClass())) {
+            FieldAccessor fieldAccessor = FieldAccessor.createAccessor(field);
+            Short classId = 
fory.getClassResolver().getRegisteredClassId(field.getType());
+            defaultFields.add(
+                new ScalaDefaultValueField(
+                    fieldName,
+                    defaultValue,
+                    fieldAccessor,
+                    classId != null ? classId : ClassResolver.NO_CLASS_ID));
+          }
+        }
+      }
+      defaultFieldsArray = defaultFields.toArray(new 
ScalaDefaultValueField[0]);
+      defaultValueFieldsCache.put(type, defaultFieldsArray);
+    } catch (Exception e) {
+      LOG.warn(
+          "Error {} building Scala default value fields for {}, default values 
support is disabled when deserializing object of type {}",
+          e.getMessage(),
+          type.getName(),
+          type.getName());
+      // Ignore exceptions and return empty array
+      defaultValueFieldsCache.put(type, new ScalaDefaultValueField[0]);
+    }
+    return defaultFieldsArray;
+  }
+
+  /**
+   * Sets default values for missing fields in a Scala case class.
+   *
+   * @param obj the object to set default values on
+   * @param scalaDefaultValueFields the cached default value fields
+   */
+  public static void setScalaDefaultValues(
+      Object obj, ScalaDefaultValueField[] scalaDefaultValueFields) {
+    for (ScalaDefaultValueField defaultField : scalaDefaultValueFields) {
+      FieldAccessor fieldAccessor = defaultField.getFieldAccessor();
+      if (fieldAccessor != null) {
+        Object defaultValue = defaultField.getDefaultValue();
+        short classId = defaultField.getClassId();
+        long fieldOffset = fieldAccessor.getFieldOffset();
+        switch (classId) {
+          case ClassResolver.PRIMITIVE_BOOLEAN_CLASS_ID:
+          case ClassResolver.BOOLEAN_CLASS_ID:
+            Platform.putBoolean(obj, fieldOffset, (Boolean) defaultValue);
+            break;
+          case ClassResolver.PRIMITIVE_BYTE_CLASS_ID:
+          case ClassResolver.BYTE_CLASS_ID:
+            Platform.putByte(obj, fieldOffset, (Byte) defaultValue);
+            break;
+          case ClassResolver.PRIMITIVE_CHAR_CLASS_ID:
+          case ClassResolver.CHAR_CLASS_ID:
+            Platform.putChar(obj, fieldOffset, (Character) defaultValue);
+            break;
+          case ClassResolver.PRIMITIVE_SHORT_CLASS_ID:
+          case ClassResolver.SHORT_CLASS_ID:
+            Platform.putShort(obj, fieldOffset, (Short) defaultValue);
+            break;
+          case ClassResolver.PRIMITIVE_INT_CLASS_ID:
+          case ClassResolver.INTEGER_CLASS_ID:
+            Platform.putInt(obj, fieldOffset, (Integer) defaultValue);
+            break;
+          case ClassResolver.PRIMITIVE_LONG_CLASS_ID:
+          case ClassResolver.LONG_CLASS_ID:
+            Platform.putLong(obj, fieldOffset, (Long) defaultValue);
+            break;
+          case ClassResolver.PRIMITIVE_FLOAT_CLASS_ID:
+          case ClassResolver.FLOAT_CLASS_ID:
+            Platform.putFloat(obj, fieldOffset, (Float) defaultValue);
+            break;
+          case ClassResolver.PRIMITIVE_DOUBLE_CLASS_ID:
+          case ClassResolver.DOUBLE_CLASS_ID:
+            Platform.putDouble(obj, fieldOffset, (Double) defaultValue);
+            break;
+          default:
+            // Object type
+            fieldAccessor.putObject(obj, defaultValue);
+        }
+      }
+    }
+  }
+
+  public static Object getDefaultValue(Class<?> cls, String fieldName) {
+    Map<String, Object> allDefaults = getAllDefaultValues(cls);
+    return allDefaults.get(fieldName);
+  }
+
+  /**
+   * Gets all default values for a Scala class. This method caches all default 
values at the class
+   * level for better performance.
+   *
+   * @param cls the Scala class
+   * @return a map from parameter index to default value (null if no default)
+   */
+  public static Map<String, Object> getAllDefaultValues(Class<?> cls) {
+    Preconditions.checkNotNull(cls, "Class must not be null");
+    // Check cache first
+    Map<String, Object> allDefaults = allDefaultValuesCache.getIfPresent(cls);
+    if (allDefaults != null) {
+      return allDefaults;
+    }
+    allDefaults = new HashMap<>();
+    // Get all constructors
+    Constructor<?>[] constructors = cls.getDeclaredConstructors();
+    // Find the constructor with the most parameters (assuming it's the 
primary constructor)
+    Constructor<?> primaryConstructor = null;
+    for (Constructor<?> constructor : constructors) {
+      if (primaryConstructor == null
+          || constructor.getParameterCount() > 
primaryConstructor.getParameterCount()) {
+        primaryConstructor = constructor;
+      }
+    }
+    Preconditions.checkNotNull(
+        primaryConstructor, "Primary constructor not found for class " + 
cls.getName());
+    Map<Integer, Object> defaultValues = getDefaultValuesForClass(cls);
+    int paramCount = primaryConstructor.getParameterCount();
+    for (int i = 0; i < paramCount; i++) {
+      String paramName = primaryConstructor.getParameters()[i].getName();
+      Object defaultValue = defaultValues.get(i + 1); // +1 because default 
values are 1-indexed
+      if (defaultValue != null) {
+        allDefaults.put(paramName, defaultValue);
+      }
+    }
+    allDefaultValuesCache.put(cls, allDefaults);
+    return allDefaults;
+  }
+
+  /**
+   * Finds all default value methods for a Scala class.
+   *
+   * @param cls the Scala class
+   * @return a map from parameter index to method handle
+   */
+  private static Map<Integer, Object> getDefaultValuesForClass(Class<?> cls) {
+    if (cachedCtrDefaultValues.getIfPresent(cls) != null) {
+      return cachedCtrDefaultValues.getIfPresent(cls);
+    }
+    Map<Integer, Object> defaultValueMethods;
+    if (ScalaTypes.isScalaProductType(cls)) {
+      defaultValueMethods = getDefaultValuesForCaseClass(cls);
+    } else {
+      defaultValueMethods = getDefaultValuesForRegularScalaClass(cls);
+    }
+    cachedCtrDefaultValues.put(cls, defaultValueMethods);
+    return defaultValueMethods;
+  }
+
+  private static Map<Integer, Object> getDefaultValuesForCaseClass(Class<?> 
cls) {
+    Map<Integer, Object> values = new HashMap<>();
+    String companionClassName = cls.getName() + "$";
+    Class<?> companionClass = null;
+    Object companionInstance = null;
+    try {
+      companionClass = Class.forName(companionClassName, false, 
cls.getClassLoader());
+      companionInstance = companionClass.getField("MODULE$").get(null);
+    } catch (Exception e) {
+      // For nested case classes, try to find the companion object in the 
enclosing class
+      Class<?> enclosingClass = cls.getEnclosingClass();
+      if (enclosingClass != null) {
+        // Look for a companion object field in the enclosing class
+        for (java.lang.reflect.Field field : 
enclosingClass.getDeclaredFields()) {
+          if (field.getType().getName().equals(companionClassName)) {
+            field.setAccessible(true);
+            try {
+              companionInstance = field.get(null);
+            } catch (Exception e1) {
+              LOG.warn(
+                  "Error {} accessing companion object for {}, default values 
support is disabled when deserializing object of type {}",
+                  e1.getMessage(),
+                  cls.getName(),
+                  cls.getName());
+              return values;
+            }
+            if (companionInstance != null) {
+              companionClass = companionInstance.getClass();
+              break;
+            }
+          }
+        }
+      }
+    }
+    if (companionClass == null) {
+      LOG.warn(
+          "Companion class not found for {}, default values support is 
disabled when deserializing object of type {}",
+          cls.getName(),
+          cls.getName());
+      return values;
+    }
+    MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(companionClass);
+
+    // Look for methods named `apply$default$1`, `apply$default$2`, etc.
+    Method[] companionMethods = companionClass.getDeclaredMethods();
+    for (Method method : companionMethods) {
+      String methodName = method.getName();
+      if (methodName.contains("$default$")) {
+        try {
+          // Extract the parameter index from the method name
+          String indexStr =
+              methodName.substring(methodName.lastIndexOf("$default$") + 
"$default$".length());
+          int paramIndex = Integer.parseInt(indexStr);
+          // Create method handle for the default value method
+          MethodHandle methodHandle = lookup.unreflect(method);
+          Object defaultValue = methodHandle.invoke(companionInstance);
+          values.put(paramIndex, defaultValue);
+        } catch (Throwable e) {
+          LOG.warn(
+              "Error: {} finding default value methods for {}, default values 
support is disabled when deserializing object of type {}",
+              e.getMessage(),
+              cls.getName(),
+              cls.getName());
+          return values;
+        }
+      }
+    }
+    return values;
+  }
+
+  private static Map<Integer, Object> 
getDefaultValuesForRegularScalaClass(Class<?> cls) {
+    Map<Integer, Object> values = new HashMap<>();
+    try {
+      MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(cls);
+      Method[] classMethods = cls.getDeclaredMethods();
+      for (Method method : classMethods) {
+        String methodName = method.getName();
+        if (methodName.contains("$default$")) {
+          try {
+            // Extract the parameter index from the method name
+            String indexStr =
+                methodName.substring(methodName.lastIndexOf("$default$") + 
"$default$".length());
+            int paramIndex = Integer.parseInt(indexStr);
+            // Create method handle for the default value method
+            MethodHandle methodHandle = lookup.unreflect(method);
+            // For regular Scala classes, we need to create an instance to 
call instance methods
+            // Since these are default value methods, we can try to call them 
as static methods
+            Object defaultValue = methodHandle.invoke();
+            values.put(paramIndex, defaultValue);
+          } catch (Throwable e) {
+            LOG.warn(
+                "Error {} finding default value for {}, default values support 
is disabled when deserializing object of type {}",
+                e.getMessage(),
+                cls.getName(),
+                cls.getName());
+            return values;
+          }
+        }
+      }
+    } catch (Exception e) {
+      LOG.warn(
+          "Error {} finding default value for {}, default values support is 
disabled when deserializing object of type {}",
+          e.getMessage(),
+          cls.getName(),
+          cls.getName());
+      return values;
+    }
+    return values;
+  }
+}
diff --git 
a/scala/src/test/scala/org/apache/fory/serializer/scala/ScalaDefaultValueTest.scala
 
b/scala/src/test/scala/org/apache/fory/serializer/scala/ScalaDefaultValueTest.scala
new file mode 100644
index 000000000..5467ac440
--- /dev/null
+++ 
b/scala/src/test/scala/org/apache/fory/serializer/scala/ScalaDefaultValueTest.scala
@@ -0,0 +1,199 @@
+/*
+ * 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.serializer.scala
+
+import org.apache.fory.Fory
+import org.apache.fory.config.Language
+import org.apache.fory.config.CompatibleMode
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+object NestedCases {
+  // Classes WITHOUT default values for serialization
+  case class NestedCaseClassNoDefaults(a: String, b: Int, c: Option[String])
+  
+  // Classes WITH default values for deserialization
+  case class NestedCaseClassWithDefaults(a: String, b: Int = 99, c: 
Option[String] = Some("nested"))
+  
+  // Simple case class with only one field for testing missing fields
+  case class SimpleNestedCase(a: String)
+}
+
+// Regular Scala classes WITHOUT default values for serialization
+class RegularScalaClassNoDefaults(val name: String) {
+  override def equals(obj: Any): Boolean = obj match {
+    case that: RegularScalaClassNoDefaults => 
+      this.name == that.name
+    case _ => false
+  }
+  
+  override def hashCode(): Int = {
+    val prime = 31
+    var result = 1
+    result = prime * result + (if (name == null) 0 else name.hashCode)
+    result
+  }
+  
+  override def toString: String = s"RegularScalaClassNoDefaults($name)"
+}
+
+// Regular Scala classes WITH default values for deserialization
+class RegularScalaClassWithDefaults(val name: String, val age: Int = 25, val 
city: String = "Unknown") {
+  override def equals(obj: Any): Boolean = obj match {
+    case that: RegularScalaClassWithDefaults => 
+      this.name == that.name && this.age == that.age && this.city == that.city
+    case _ => false
+  }
+  
+  override def hashCode(): Int = {
+    val prime = 31
+    var result = 1
+    result = prime * result + (if (name == null) 0 else name.hashCode)
+    result = prime * result + age
+    result = prime * result + (if (city == null) 0 else city.hashCode)
+    result
+  }
+  
+  override def toString: String = s"RegularScalaClassWithDefaults($name, $age, 
$city)"
+}
+
+class ScalaDefaultValueTest extends AnyWordSpec with Matchers {
+  
+  // Test both runtime mode (MetaSharedSerializer) and codegen mode 
(MetaSharedCodecBuilder)
+  val testModes = Seq(
+    ("Runtime Mode", false),
+    ("Codegen Mode", true)
+  )
+
+  def createFory(codegen: Boolean): Fory = Fory.builder()
+    .withLanguage(Language.JAVA)
+    .withRefTracking(true)
+    .withScalaOptimizationEnabled(true)
+    .requireClassRegistration(false)
+    .suppressClassRegistrationWarnings(false)
+    .withCodegen(codegen)
+    .withCompatibleMode(CompatibleMode.COMPATIBLE)
+    .build()
+
+  "Fory Scala default value support" should {
+    testModes.foreach { case (modeName, codegen) =>
+      s"handle missing fields with default values in case class in $modeName" 
in {
+        val fory = createFory(codegen)
+        // Serialize object WITHOUT the x field (it doesn't exist in source 
class)
+        val original = CaseClassNoDefaults("test")
+        val serialized = fory.serialize(original)
+        
+        // Deserialize into class WITH default values for x
+        val deserialized = fory.deserialize(serialized, 
classOf[CaseClassWithDefaults])
+        deserialized.v shouldEqual "test"
+        deserialized.x shouldEqual 1 // Should use default value
+      }
+
+      s"handle multiple missing fields with default values in case class in 
$modeName" in {
+        val fory = createFory(codegen)
+        // Serialize object WITHOUT x and y fields (they don't exist in source 
class)
+        val original = CaseClassMultipleDefaultsNoDefaults("test")
+        val serialized = fory.serialize(original)
+        
+        // Deserialize into class WITH default values for x and y
+        val deserialized = fory.deserialize(serialized, 
classOf[CaseClassMultipleDefaultsWithDefaults])
+        deserialized.v shouldEqual "test"
+        deserialized.x shouldEqual 1 // Should use default value
+        deserialized.y shouldEqual 2.0 // Should use default value
+      }
+
+      s"work with complex default values in case class in $modeName" in {
+        val fory = createFory(codegen)
+        // Serialize object WITHOUT the list field (it doesn't exist in source 
class)
+        val original = CaseClassComplexDefaultsNoDefaults("test")
+        val serialized = fory.serialize(original)
+        
+        // Deserialize into class WITH default values for list
+        val deserialized = fory.deserialize(serialized, 
classOf[CaseClassComplexDefaultsWithDefaults])
+        deserialized.v shouldEqual "test"
+        deserialized.list shouldEqual List(1, 2, 3) // Should use default value
+      }
+
+      s"handle partial missing fields with default values in case class in 
$modeName" in {
+        val fory = createFory(codegen)
+        // This test case needs to be updated since we can't have partial 
fields
+        // For now, we'll test with a different approach - serialize with one 
field missing
+        val original = CaseClassNoDefaults("test") // Only has v field
+        val serialized = fory.serialize(original)
+        
+        // Deserialize into class WITH default values for x
+        val deserialized = fory.deserialize(serialized, 
classOf[CaseClassWithDefaults])
+        deserialized.v shouldEqual "test"
+        deserialized.x shouldEqual 1 // Should use default value
+      }
+
+      s"handle missing fields with default values in nested case class in 
$modeName" in {
+        val fory = createFory(codegen)
+        import NestedCases._
+        // Create a case class with only the first field, simulating missing b 
and c fields
+        // We'll use a different approach - serialize a simpler object and 
deserialize into a more complex one
+        val original = SimpleNestedCase("nestedTest")
+        val serialized = fory.serialize(original)
+        
+        // Deserialize into class WITH default values for b and c
+        val deserialized = fory.deserialize(serialized, 
classOf[NestedCaseClassWithDefaults])
+        deserialized.a shouldEqual "nestedTest"
+        deserialized.b shouldEqual 99 // Should use default value
+        deserialized.c shouldEqual Some("nested") // Should use default value
+      }
+
+      s"handle missing fields with default values in regular Scala class in 
$modeName" in {
+        val fory = createFory(codegen)
+        // Serialize object WITHOUT default values (missing age and city 
fields)
+        val original = new RegularScalaClassNoDefaults("Jane")
+        val serialized = fory.serialize(original)
+        
+        // Deserialize into class WITH default values
+        val deserialized = fory.deserialize(serialized, 
classOf[RegularScalaClassWithDefaults])
+        deserialized.name shouldEqual "Jane"
+        deserialized.age shouldEqual 25 // Should use default value
+        deserialized.city shouldEqual "Unknown" // Should use default value
+      }
+
+      s"handle partial missing fields with default values in regular Scala 
class in $modeName" in {
+        val fory = createFory(codegen)
+        // Serialize object with only name field, missing age and city
+        val original = new RegularScalaClassNoDefaults("Bob")
+        val serialized = fory.serialize(original)
+        
+        // Deserialize into class WITH default values
+        val deserialized = fory.deserialize(serialized, 
classOf[RegularScalaClassWithDefaults])
+        deserialized.name shouldEqual "Bob"
+        deserialized.age shouldEqual 25 // Should use default value
+        deserialized.city shouldEqual "Unknown" // Should use default value
+      }
+    }
+  }
+}
+
+// Test case classes WITHOUT default values (for serialization)
+case class CaseClassNoDefaults(v: String) // No x field at all
+case class CaseClassMultipleDefaultsNoDefaults(v: String) // No x and y fields 
at all
+case class CaseClassComplexDefaultsNoDefaults(v: String) // No list field at 
all
+
+// Test case classes WITH default values (for deserialization)
+case class CaseClassWithDefaults(v: String, x: Int = 1)
+case class CaseClassMultipleDefaultsWithDefaults(v: String, x: Int = 1, y: 
Double = 2.0)
+case class CaseClassComplexDefaultsWithDefaults(v: String, list: List[Int] = 
List(1, 2, 3)) 
\ No newline at end of file
diff --git 
a/scala/src/test/scala/org/apache/fory/util/ScalaDefaultValueUtilsTest.scala 
b/scala/src/test/scala/org/apache/fory/util/ScalaDefaultValueUtilsTest.scala
new file mode 100644
index 000000000..a487faf76
--- /dev/null
+++ b/scala/src/test/scala/org/apache/fory/util/ScalaDefaultValueUtilsTest.scala
@@ -0,0 +1,383 @@
+/*
+ * 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.util
+
+import org.apache.fory.Fory
+import org.apache.fory.config.Language
+import org.apache.fory.`type`.Descriptor
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import org.scalatest.Succeeded
+import org.scalatest.matchers.should.Matchers._
+import java.util.{List => JavaList, ArrayList}
+import scala.jdk.CollectionConverters._
+
+// Test case classes WITH default values for testing ScalaDefaultValueUtils
+case class TestCaseClassWithDefaults(
+  name: String,
+  age: Int = 25,
+  city: String = "Unknown",
+  active: Boolean = true,
+  score: Double = 0.0,
+  tags: List[String] = List("default")
+)
+
+case class TestCaseClassMultipleDefaults(
+  id: Int,
+  name: String = "default",
+  description: String = "no description",
+  count: Int = 0,
+  enabled: Boolean = false
+)
+
+case class TestCaseClassComplexDefaults(
+  title: String,
+  metadata: Map[String, String] = Map("type" -> "default"),
+  numbers: List[Int] = List(1, 2, 3),
+  optional: Option[String] = Some("default")
+)
+
+// Test case classes WITHOUT default values (for comparison)
+case class TestCaseClassNoDefaults(name: String, age: Int, city: String)
+
+// Regular Scala classes WITH default values
+class TestRegularScalaClassWithDefaults(
+  val name: String,
+  val age: Int = 30,
+  val city: String = "DefaultCity",
+  val active: Boolean = false
+) {
+  override def equals(obj: Any): Boolean = obj match {
+    case that: TestRegularScalaClassWithDefaults =>
+      this.name == that.name && this.age == that.age && 
+      this.city == that.city && this.active == that.active
+    case _ => false
+  }
+  
+  override def hashCode(): Int = {
+    val prime = 31
+    var result = 1
+    result = prime * result + (if (name == null) 0 else name.hashCode)
+    result = prime * result + age
+    result = prime * result + (if (city == null) 0 else city.hashCode)
+    result = prime * result + (if (active) 1 else 0)
+    result
+  }
+  
+  override def toString: String = s"TestRegularScalaClassWithDefaults($name, 
$age, $city, $active)"
+}
+
+// Regular Scala classes WITHOUT default values
+class TestRegularScalaClassNoDefaults(val name: String, val age: Int) {
+  override def equals(obj: Any): Boolean = obj match {
+    case that: TestRegularScalaClassNoDefaults =>
+      this.name == that.name && this.age == that.age
+    case _ => false
+  }
+  
+  override def hashCode(): Int = {
+    val prime = 31
+    var result = 1
+    result = prime * result + (if (name == null) 0 else name.hashCode)
+    result = prime * result + age
+    result
+  }
+  
+  override def toString: String = s"TestRegularScalaClassNoDefaults($name, 
$age)"
+}
+
+// Java-like class for testing non-Scala classes
+class TestJavaClass(val name: String, val age: Int) {
+  override def equals(obj: Any): Boolean = obj match {
+    case that: TestJavaClass =>
+      this.name == that.name && this.age == that.age
+    case _ => false
+  }
+  
+  override def hashCode(): Int = {
+    val prime = 31
+    var result = 1
+    result = prime * result + (if (name == null) 0 else name.hashCode)
+    result = prime * result + age
+    result
+  }
+  
+  override def toString: String = s"TestJavaClass($name, $age)"
+}
+
+// Object to contain truly nested case classes
+object NestedClasses {
+  case class NestedCaseClass(
+    outer: String,
+    inner: TestCaseClassWithDefaults = TestCaseClassWithDefaults("nested")
+  )
+
+  case class DeeplyNestedCaseClass(
+    level1: String,
+    level2: NestedCaseClass = NestedCaseClass("level2", 
TestCaseClassWithDefaults("deep")),
+    level3: TestCaseClassMultipleDefaults = TestCaseClassMultipleDefaults(999, 
"deep3")
+  )
+
+  case class NestedCaseClassNoDefaults(
+    outer: String,
+    inner: TestCaseClassNoDefaults
+  )
+}
+
+class ScalaDefaultValueUtilsTest extends AnyWordSpec with Matchers {
+
+  def createFory(): Fory = Fory.builder()
+    .withLanguage(Language.JAVA)
+    .withRefTracking(true)
+    .withScalaOptimizationEnabled(true)
+    .requireClassRegistration(false)
+    .suppressClassRegistrationWarnings(false)
+    .build()
+
+  /**
+   * Helper function to get default values as a Scala Map for easier testing
+   */
+  def getDefaultValuesAsScalaMap[T](cls: Class[T]): Map[String, Object] = {
+    ScalaDefaultValueUtils.getAllDefaultValues(cls).asScala.toMap
+  }
+
+  /**
+   * Helper function to build Scala default value fields for easier testing
+   */
+  def buildDefaultValueFields[T](cls: Class[T]): 
Array[ScalaDefaultValueUtils.ScalaDefaultValueField] = {
+    val fory = createFory()
+    val descriptors = new ArrayList[Descriptor]()
+    ScalaDefaultValueUtils.buildScalaDefaultValueFields(fory, cls, descriptors)
+  }
+
+  "ScalaDefaultValueUtils" should {
+
+    "detect Scala classes with default values correctly" in {
+      // Test case classes with default values
+      
ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[TestCaseClassWithDefaults])
 shouldEqual true
+      
ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[TestCaseClassMultipleDefaults])
 shouldEqual true
+      
ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[TestCaseClassComplexDefaults])
 shouldEqual true
+      
+      // Test regular Scala classes with default values
+      
ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[TestRegularScalaClassWithDefaults])
 shouldEqual true
+      
+      // Test classes without default values
+      
ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[TestCaseClassNoDefaults]) 
shouldEqual false
+      
ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[TestRegularScalaClassNoDefaults])
 shouldEqual false
+      ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[TestJavaClass]) 
shouldEqual false
+      
+             // Test built-in types
+       ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[String]) 
shouldEqual false
+       // Skip primitive types as they don't have constructors
+       ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[List[_]]) 
shouldEqual false
+    }
+
+    "get all default values for Scala classes" in {
+       // Test case class with defaults
+       val caseClassDefaults = 
getDefaultValuesAsScalaMap(classOf[TestCaseClassWithDefaults])
+       caseClassDefaults should not be empty
+       caseClassDefaults("age") shouldEqual 25
+       caseClassDefaults("city") shouldEqual "Unknown"
+       caseClassDefaults("active") shouldEqual true
+       caseClassDefaults("score") shouldEqual 0.0
+       caseClassDefaults("tags") shouldEqual List("default")
+       
+       // Test case class with multiple defaults
+       val multipleDefaults = 
getDefaultValuesAsScalaMap(classOf[TestCaseClassMultipleDefaults])
+       multipleDefaults should not be empty
+       multipleDefaults("name") shouldEqual "default"
+       multipleDefaults("description") shouldEqual "no description"
+       multipleDefaults("count") shouldEqual 0
+       multipleDefaults("enabled") shouldEqual false
+       
+       // Test case class with complex defaults
+       val complexDefaults = 
getDefaultValuesAsScalaMap(classOf[TestCaseClassComplexDefaults])
+       complexDefaults should not be empty
+       // Handle the metadata map - it might be a Scala Map or Java Map
+       val metadataValue = complexDefaults("metadata")
+       metadataValue should not be null
+       // Check if it's a Scala Map or Java Map and handle accordingly
+       metadataValue match {
+         case javaMap: java.util.Map[String, String] =>
+           javaMap.asScala.toMap shouldEqual Map("type" -> "default")
+         case scalaMap: Map[String, String] =>
+           scalaMap shouldEqual Map("type" -> "default")
+         case _ =>
+           fail(s"Unexpected metadata type: ${metadataValue.getClass}")
+       }
+       complexDefaults("numbers") shouldEqual List(1, 2, 3)
+       complexDefaults("optional") shouldEqual Some("default")
+       
+       // Test regular Scala class with defaults
+       val regularDefaults = 
getDefaultValuesAsScalaMap(classOf[TestRegularScalaClassWithDefaults])
+       regularDefaults should not be empty
+       regularDefaults("age") shouldEqual 30
+       regularDefaults("city") shouldEqual "DefaultCity"
+       regularDefaults("active") shouldEqual false
+       
+       // Test classes without defaults
+       getDefaultValuesAsScalaMap(classOf[TestCaseClassNoDefaults]) shouldBe 
empty
+       getDefaultValuesAsScalaMap(classOf[TestRegularScalaClassNoDefaults]) 
shouldBe empty
+       getDefaultValuesAsScalaMap(classOf[TestJavaClass]) shouldBe empty
+     }
+
+    "build Scala default value fields correctly" in {
+       // Test with case class that has defaults
+       val fields = buildDefaultValueFields(classOf[TestCaseClassWithDefaults])
+       
+       fields should not be empty
+       fields.foreach { field =>
+         field.getFieldName should not be null
+         field.getDefaultValue should not be null
+         field.getFieldAccessor should not be null
+       }
+       
+       // Test with case class without defaults
+       val noDefaultFields = 
buildDefaultValueFields(classOf[TestCaseClassNoDefaults])
+       noDefaultFields shouldBe empty
+       
+       // Test with Java class
+       val javaFields = buildDefaultValueFields(classOf[TestJavaClass])
+       javaFields shouldBe empty
+     }
+
+    "set Scala default values on objects correctly" in {
+       val fory = createFory()
+       
+       // Create an object with missing fields
+       val obj = new TestCaseClassWithDefaults("test", 0, null, false, 0.0, 
null)
+       
+       // Build default value fields
+       val descriptors = new ArrayList[Descriptor]()
+       val fields = ScalaDefaultValueUtils.buildScalaDefaultValueFields(
+         fory, classOf[TestCaseClassWithDefaults], descriptors)
+       
+       // Set default values
+       ScalaDefaultValueUtils.setScalaDefaultValues(obj, fields)
+       
+       // Verify default values were set
+       obj.age shouldEqual 25
+       obj.city shouldEqual "Unknown"
+       obj.active shouldEqual true
+       obj.score shouldEqual 0.0
+       obj.tags shouldEqual List("default")
+     }
+
+    "cache results for better performance" in {
+      // Test that getAllDefaultValues caches results
+      val firstCall = 
ScalaDefaultValueUtils.getAllDefaultValues(classOf[TestCaseClassWithDefaults])
+      val secondCall = 
ScalaDefaultValueUtils.getAllDefaultValues(classOf[TestCaseClassWithDefaults])
+      
+      // Should return the same cached result
+      firstCall should be theSameInstanceAs secondCall
+      
+             // Test that buildScalaDefaultValueFields caches results
+       val fory = createFory()
+       val descriptors = new ArrayList[Descriptor]()
+       
+       val firstFields = ScalaDefaultValueUtils.buildScalaDefaultValueFields(
+         fory, classOf[TestCaseClassWithDefaults], descriptors)
+       val secondFields = ScalaDefaultValueUtils.buildScalaDefaultValueFields(
+         fory, classOf[TestCaseClassWithDefaults], descriptors)
+      
+      // Should return the same cached result
+      firstFields should be theSameInstanceAs secondFields
+    }
+
+    "handle different field types correctly" in {
+       // Test with different field types
+       val fields = buildDefaultValueFields(classOf[TestCaseClassWithDefaults])
+       
+       fields.foreach { field =>
+         field.getDefaultValue should not be null
+         field.getFieldName should not be null
+         field.getFieldAccessor should not be null
+         field.getClassId.toInt should be >= 0
+       }
+     }
+
+    "work with nested case classes" in {
+       import NestedClasses._
+       // Nested case classes with default values should be detected
+       ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[NestedCaseClass]) 
shouldEqual true
+       
+       val nestedDefaults = 
getDefaultValuesAsScalaMap(classOf[NestedCaseClass])
+       nestedDefaults should not be empty
+       nestedDefaults("inner") shouldEqual TestCaseClassWithDefaults("nested")
+     }
+
+     "work with deeply nested case classes" in {
+       import NestedClasses._
+       // Deeply nested case classes with default values should be detected
+       
ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[DeeplyNestedCaseClass]) 
shouldEqual true
+       
+       val deepDefaults = 
getDefaultValuesAsScalaMap(classOf[DeeplyNestedCaseClass])
+       deepDefaults should not be empty
+       deepDefaults("level2") shouldEqual NestedCaseClass("level2", 
TestCaseClassWithDefaults("deep"))
+       deepDefaults("level3") shouldEqual TestCaseClassMultipleDefaults(999, 
"deep3")
+     }
+
+     "work with nested case classes without defaults" in {
+       import NestedClasses._
+       // Nested case classes without default values should not be detected
+       
ScalaDefaultValueUtils.hasScalaDefaultValues(classOf[NestedCaseClassNoDefaults])
 shouldEqual false
+       
+       val nestedDefaults = 
getDefaultValuesAsScalaMap(classOf[NestedCaseClassNoDefaults])
+       nestedDefaults shouldBe empty
+     }
+
+    "handle error cases gracefully" in {
+       val fory = createFory()
+       
+       // Test with classes that might cause reflection issues
+       val descriptors = new ArrayList[Descriptor]()
+       
+       // These should not throw exceptions
+       noException should be thrownBy {
+         ScalaDefaultValueUtils.buildScalaDefaultValueFields(fory, 
classOf[Object], descriptors)
+       }
+       
+       noException should be thrownBy {
+         ScalaDefaultValueUtils.buildScalaDefaultValueFields(fory, 
classOf[String], descriptors)
+       }
+       
+       noException should be thrownBy {
+         ScalaDefaultValueUtils.buildScalaDefaultValueFields(fory, 
classOf[List[_]], descriptors)
+       }
+     }
+
+    "test ScalaDefaultValueField inner class" in {
+       // Test the getter methods of ScalaDefaultValueField
+       // We can't create instances directly due to private constructor,
+       // but we can test the getters if we get an instance from the utility 
methods
+       
+       // Try to get fields from a class with defaults
+       val fields = buildDefaultValueFields(classOf[TestCaseClassWithDefaults])
+       
+       if (fields.nonEmpty) {
+         val field = fields(0)
+         field.getFieldName should not be null
+         field.getDefaultValue should not be null
+         field.getFieldAccessor should not be null
+         field.getClassId.toInt should be >= 0
+       }
+     }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@fory.apache.org
For additional commands, e-mail: commits-h...@fory.apache.org

Reply via email to