You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/08/14 04:35:22 UTC

(fory) branch main updated: feat(scala): add scala2 json enumeration annotation (#3935)

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 1d22fc8c7 feat(scala): add scala2 json enumeration annotation (#3935)
1d22fc8c7 is described below

commit 1d22fc8c7021142b4a13b85f0aa5d334c13d92da
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Fri Aug 14 12:35:17 2026 +0800

    feat(scala): add scala2 json enumeration annotation (#3935)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence or equivalent persisted links of the
    final clean AI review results from both fresh reviewers described in
    `AI_POLICY.md`, the Fory-guided reviewer and the independent general
    reviewer, on the current PR diff or current HEAD after the latest code
    changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 docs/json/scala.md                                 |  27 +-
 docs/security/deserialization.md                   |  20 ++
 .../org/apache/fory/json/codec/ArrayCodec.java     |   8 +-
 .../apache/fory/json/codec/JsonObjectModel.java    |  81 ++++-
 .../apache/fory/json/codec/ObjectCodecBuilder.java |  88 +++++-
 .../org/apache/fory/json/meta/JsonFieldInfo.java   |  17 +-
 .../fory/json/resolver/JsonSharedRegistry.java     |   2 +-
 .../apache/fory/json/ForyJsonGraalVMFeature.java   |  73 +++++
 scala/build.sbt                                    |   1 +
 .../apache/fory/json/scala/JsonEnumeration.java    |  53 ++++
 .../scala/internal/ScalaEnumerationTypes.scala     | 346 +++++++++++++++++++++
 .../scala/internal/ScalaEnumerationTypes.scala     |  71 +++++
 .../scala/internal/ScalaCollectionCodecs.scala     |  17 +-
 .../json/scala/internal/ScalaObjectModels.scala    |  11 +-
 .../scala/internal/ScalaTypeCodecFactory.scala     |   2 +
 .../ScalaJsonEnumerationNativeImageMain.scala      |  50 +++
 .../json/scala/ScalaJsonEnumerationSuite.scala     | 145 +++++++++
 17 files changed, 982 insertions(+), 30 deletions(-)

diff --git a/docs/json/scala.md b/docs/json/scala.md
index 06218c0c4..6cd6e6880 100644
--- a/docs/json/scala.md
+++ b/docs/json/scala.md
@@ -132,23 +132,38 @@ value decodes to `Some(value)`; JSON `null` is rejected for `Some[Int]` but deco
 
 ## Scala 2 Enumeration
 
-Scala 2 erases the owning `Enumeration` from `Enumeration#Value`. Bind the owner explicitly with
-`ScalaEnumerationCodec` and select it through an annotation:
+Scala 2 erases the owning `Enumeration` from `Enumeration#Value`. Use `JsonEnumeration` to retain
+the owner on a direct value, collection or array element, `Option` content, or map key/value:
 
 ```scala
+import org.apache.fory.json.scala.JsonEnumeration
+
 object Weekday extends Enumeration {
   val Monday, Tuesday = Value
 }
 
-final class WeekdayCodec extends ScalaEnumerationCodec(Weekday)
+object Month extends Enumeration {
+  val January, February = Value
+}
 
 case class Schedule(
-    @JsonCodec(value = classOf[WeekdayCodec]) day: Weekday.Value,
-    @JsonCodec(elementCodec = classOf[WeekdayCodec]) days: List[Weekday.Value]
+    @JsonEnumeration(classOf[Weekday.type]) day: Weekday.Value,
+    @JsonEnumeration(element = classOf[Weekday.type]) days: List[Weekday.Value],
+    @JsonEnumeration(content = classOf[Month.type]) month: Option[Month.Value],
+    @JsonEnumeration(
+      mapKey = classOf[Weekday.type],
+      mapValue = classOf[Month.type]
+    ) labels: Map[Weekday.Value, Month.Value]
 )
 ```
 
-The codec also implements the map-key contract, so its class can be used in `keyCodec`.
+Each slot describes one direct `Enumeration.Value` occurrence. `value` cannot be combined with a
+child slot, and `element`, `content`, and map slots must match the annotated property's immediate
+type shape. Invalid or conflicting declarations fail when the case-class metadata is created.
+
+For a custom wire representation, extend `ScalaEnumerationCodec` and select the codec through
+`@JsonCodec`. The codec also implements the map-key contract, so its class can be used in
+`keyCodec`.
 
 ## Scala 3 closed enums
 
diff --git a/docs/security/deserialization.md b/docs/security/deserialization.md
index 6eae0b2f9..827240c66 100644
--- a/docs/security/deserialization.md
+++ b/docs/security/deserialization.md
@@ -149,6 +149,26 @@ When a path cannot produce one of these outcomes, earlier rejection of malformed
 bytes is normally a correctness or interoperability choice, not a security
 requirement.
 
+## Reader Limits Do Not Imply Writer Limits
+
+Read-side length, scale, count, depth, memory, and work limits protect
+deserialization from untrusted input. They are not automatically value-range
+contracts for serialization and do not require a corresponding writer-side
+check.
+
+In particular, Java Fory JSON's arbitrary-precision number length and decimal
+scale limits bound reader-side parsing and materialization. Writers must not
+reject an in-memory `BigInteger`, `BigDecimal`, or language wrapper solely
+because the default reader would reject the resulting JSON. Round-trip
+symmetry, matching error behavior, and test uniformity do not justify extra
+writer branches or validation.
+
+A writer-side restriction is appropriate only when the wire specification or a
+public write API explicitly requires it, or when writing the value otherwise
+causes a concrete writer-owned runtime-safety or resource failure. Such a check
+belongs to the writer that owns that boundary and must not be inferred from a
+deserialization limit.
+
 ## Robustness Scope Gate
 
 Before reporting or fixing a deserialization robustness finding, establish a
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java
index b109b769e..a84fe034f 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java
@@ -31,6 +31,7 @@ import org.apache.fory.json.resolver.JsonTypeInfo;
 import org.apache.fory.json.resolver.JsonTypeResolver;
 import org.apache.fory.json.writer.StringJsonWriter;
 import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.apache.fory.reflect.TypeRef;
 import org.apache.fory.serializer.GraphMemoryEstimates;
 import org.apache.fory.type.TypeUtils;
 
@@ -55,12 +56,15 @@ public abstract class ArrayCodec<T> implements JsonValueCodec<T> {
     this.componentType = componentType;
   }
 
-  public static <T> ArrayCodec<T> create(Class<T> arrayType, JsonTypeResolver resolver) {
+  public static <T> ArrayCodec<T> create(
+      Class<T> arrayType, TypeRef<?> arrayTypeRef, JsonTypeResolver resolver) {
     if (!arrayType.isArray()) {
       throw new ForyJsonException("Unsupported JSON array type " + arrayType);
     }
     Class<?> componentType = arrayType.getComponentType();
-    JsonTypeInfo componentTypeInfo = resolver.getTypeInfo(componentType, componentType);
+    TypeRef<?> componentTypeRef = arrayTypeRef.getComponentType();
+    JsonTypeInfo componentTypeInfo =
+        resolver.getTypeInfo(componentTypeRef.getType(), componentType);
     return create(arrayType, componentTypeInfo);
   }
 
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonObjectModel.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonObjectModel.java
index 0dbcce2b6..0f7fc9a50 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonObjectModel.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/JsonObjectModel.java
@@ -21,6 +21,7 @@ package org.apache.fory.json.codec;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
+import java.lang.reflect.Type;
 import java.util.HashSet;
 import java.util.Objects;
 import org.apache.fory.annotation.Internal;
@@ -35,13 +36,22 @@ public final class JsonObjectModel {
   private final String[] propertyNames;
   private final Method[] propertyGetters;
   private final Method[] propertySetters;
+  private final Type[] propertyTypes;
 
   public JsonObjectModel(
       Constructor<?> constructor,
       String[] parameterNames,
       Method[] accessors,
       Method[] defaultMethods) {
-    this(constructor, parameterNames, accessors, defaultMethods, parameterNames, accessors, null);
+    this(
+        constructor,
+        parameterNames,
+        accessors,
+        defaultMethods,
+        parameterNames,
+        accessors,
+        null,
+        constructor.getGenericParameterTypes());
   }
 
   public JsonObjectModel(
@@ -52,6 +62,27 @@ public final class JsonObjectModel {
       String[] propertyNames,
       Method[] propertyGetters,
       Method[] propertySetters) {
+    this(
+        constructor,
+        parameterNames,
+        accessors,
+        defaultMethods,
+        propertyNames,
+        propertyGetters,
+        propertySetters,
+        propertyTypes(
+            constructor, parameterNames, propertyNames, propertyGetters, propertySetters));
+  }
+
+  public JsonObjectModel(
+      Constructor<?> constructor,
+      String[] parameterNames,
+      Method[] accessors,
+      Method[] defaultMethods,
+      String[] propertyNames,
+      Method[] propertyGetters,
+      Method[] propertySetters,
+      Type[] propertyTypes) {
     this.constructor = Objects.requireNonNull(constructor, "constructor");
     this.parameterNames = parameterNames.clone();
     this.accessors = accessors.clone();
@@ -60,6 +91,7 @@ public final class JsonObjectModel {
     this.propertyGetters = propertyGetters.clone();
     this.propertySetters =
         propertySetters == null ? new Method[propertyNames.length] : propertySetters.clone();
+    this.propertyTypes = propertyTypes.clone();
     int count = constructor.getParameterCount();
     if (this.parameterNames.length != count
         || this.accessors.length != count
@@ -73,7 +105,8 @@ public final class JsonObjectModel {
       }
     }
     if (this.propertyGetters.length != this.propertyNames.length
-        || this.propertySetters.length != this.propertyNames.length) {
+        || this.propertySetters.length != this.propertyNames.length
+        || this.propertyTypes.length != this.propertyNames.length) {
       throw new IllegalArgumentException(
           "JSON object model property arrays must have equal length");
     }
@@ -83,6 +116,9 @@ public final class JsonObjectModel {
         throw new IllegalArgumentException("Invalid JSON object model property name " + name);
       }
     }
+    for (Type propertyType : this.propertyTypes) {
+      Objects.requireNonNull(propertyType, "propertyType");
+    }
   }
 
   public Constructor<?> constructor() {
@@ -112,4 +148,45 @@ public final class JsonObjectModel {
   public Method[] propertySetters() {
     return propertySetters.clone();
   }
+
+  public Type[] propertyTypes() {
+    return propertyTypes.clone();
+  }
+
+  private static Type[] propertyTypes(
+      Constructor<?> constructor,
+      String[] parameterNames,
+      String[] propertyNames,
+      Method[] propertyGetters,
+      Method[] propertySetters) {
+    Method[] setters = propertySetters == null ? new Method[propertyNames.length] : propertySetters;
+    if (propertyGetters.length != propertyNames.length || setters.length != propertyNames.length) {
+      throw new IllegalArgumentException(
+          "JSON object model property arrays must have equal length");
+    }
+    Type[] parameterTypes = constructor.getGenericParameterTypes();
+    Type[] types = new Type[propertyNames.length];
+    for (int i = 0; i < types.length; i++) {
+      for (int parameterIndex = 0; parameterIndex < parameterNames.length; parameterIndex++) {
+        if (propertyNames[i].equals(parameterNames[parameterIndex])) {
+          types[i] = parameterTypes[parameterIndex];
+          break;
+        }
+      }
+      if (types[i] != null) {
+        continue;
+      }
+      Method getter = propertyGetters[i];
+      Method setter = setters[i];
+      if (getter != null) {
+        types[i] = getter.getGenericReturnType();
+      } else if (setter != null && setter.getParameterCount() == 1) {
+        types[i] = setter.getGenericParameterTypes()[0];
+      } else {
+        throw new IllegalArgumentException(
+            "JSON object model property has no type source " + propertyNames[i]);
+      }
+    }
+    return types;
+  }
 }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
index c615934a6..4bf24b6d2 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
@@ -24,10 +24,12 @@ import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Executable;
 import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
 import java.lang.reflect.Member;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.Parameter;
+import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -1084,18 +1086,12 @@ final class ObjectCodecBuilder {
     String[] names = objectModel.propertyNames();
     Method[] accessors = objectModel.propertyGetters();
     Method[] setters = objectModel.propertySetters();
-    String[] parameterNames = objectModel.parameterNames();
-    Type[] parameterTypes = objectModel.constructor().getGenericParameterTypes();
+    Type[] propertyTypes = objectModel.propertyTypes();
     for (int i = 0; i < names.length; i++) {
       Method accessor = accessors[i];
       FieldBuilder builder =
           builders.computeIfAbsent(names[i], name -> new FieldBuilder(name, annotations));
-      for (int parameterIndex = 0; parameterIndex < parameterNames.length; parameterIndex++) {
-        if (names[i].equals(parameterNames[parameterIndex])) {
-          builder.setObjectModelType(parameterTypes[parameterIndex]);
-          break;
-        }
-      }
+      builder.setObjectModelType(propertyTypes[i]);
       if (accessor != null) {
         builder.setWriteGetter(type, accessor);
       }
@@ -1412,7 +1408,7 @@ final class ObjectCodecBuilder {
         continue;
       }
       if (!builder.isAny() && builder.unwrappedAnnotation == null) {
-        Type resolved = ownerType.resolveType(parameterTypes[i]).getType();
+        Type resolved = builder.logicalType(ownerType);
         fields.add(
             new JsonCreatorFieldInfo(
                 builder.jsonName(namingStrategy),
@@ -1494,7 +1490,9 @@ final class ObjectCodecBuilder {
       FieldBuilder builder) {
     Type resolvedParameter = ownerType.resolveType(parameterType).getType();
     Type propertyType = builder.logicalType(ownerType);
-    if (!resolvedParameter.equals(propertyType)) {
+    if (!resolvedParameter.equals(propertyType)
+        && (builder.objectModelType == null
+            || !sameObjectModelShape(resolvedParameter, propertyType))) {
       throw new ForyJsonException(
           "@JsonCreator parameter type "
               + resolvedParameter
@@ -1510,6 +1508,71 @@ final class ObjectCodecBuilder {
     builder.creatorArgumentIndex = parameterIndex;
   }
 
+  private static boolean sameObjectModelShape(Type left, Type right) {
+    if (left.equals(right)) {
+      return true;
+    }
+    if (left instanceof Class<?> && ((Class<?>) left).isArray()) {
+      Type rightComponent = arrayComponent(right);
+      return rightComponent != null
+          && sameObjectModelShape(((Class<?>) left).getComponentType(), rightComponent);
+    }
+    if (right instanceof Class<?> && ((Class<?>) right).isArray()) {
+      Type leftComponent = arrayComponent(left);
+      return leftComponent != null
+          && sameObjectModelShape(leftComponent, ((Class<?>) right).getComponentType());
+    }
+    if (left instanceof GenericArrayType || right instanceof GenericArrayType) {
+      Type leftComponent = arrayComponent(left);
+      Type rightComponent = arrayComponent(right);
+      return leftComponent != null
+          && rightComponent != null
+          && sameObjectModelShape(leftComponent, rightComponent);
+    }
+    Class<?> leftRaw = objectModelRawType(left);
+    Class<?> rightRaw = objectModelRawType(right);
+    if (leftRaw == null || leftRaw != rightRaw) {
+      return false;
+    }
+    Type[] leftArguments = typeArguments(left);
+    Type[] rightArguments = typeArguments(right);
+    if (leftArguments.length != rightArguments.length) {
+      return false;
+    }
+    for (int i = 0; i < leftArguments.length; i++) {
+      if (!sameObjectModelShape(leftArguments[i], rightArguments[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static Type arrayComponent(Type type) {
+    if (type instanceof Class<?> && ((Class<?>) type).isArray()) {
+      return ((Class<?>) type).getComponentType();
+    }
+    return type instanceof GenericArrayType
+        ? ((GenericArrayType) type).getGenericComponentType()
+        : null;
+  }
+
+  private static Type[] typeArguments(Type type) {
+    return type instanceof ParameterizedType
+        ? ((ParameterizedType) type).getActualTypeArguments()
+        : new Type[0];
+  }
+
+  private static Class<?> objectModelRawType(Type type) {
+    if (type instanceof Class<?>) {
+      return (Class<?>) type;
+    }
+    if (type instanceof ParameterizedType) {
+      Type rawType = ((ParameterizedType) type).getRawType();
+      return rawType instanceof Class<?> ? (Class<?>) rawType : null;
+    }
+    return null;
+  }
+
   private static void rejectCreatorHashCollisions(JsonCreatorFieldInfo[] fields) {
     Map<Long, String> names = new LinkedHashMap<>();
     for (JsonCreatorFieldInfo field : fields) {
@@ -2748,6 +2811,7 @@ final class ObjectCodecBuilder {
           writeAccessor,
           readAccessor,
           ownerType,
+          objectModelType,
           codecAnnotation,
           valueCodecClass,
           formatAnnotation,
@@ -3096,8 +3160,8 @@ final class ObjectCodecBuilder {
         if (writeType == void.class) {
           writeType = modelType;
         }
-        if (writeType != null && !writeType.equals(modelType)
-            || readType != null && !readType.equals(modelType)) {
+        if (writeType != null && !sameObjectModelShape(writeType, modelType)
+            || readType != null && !sameObjectModelShape(readType, modelType)) {
           throw new ForyJsonException(
               "JSON object-model type " + modelType + " does not match property " + name);
         }
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
index 7d74e7361..9a20c9253 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java
@@ -150,6 +150,7 @@ public final class JsonFieldInfo {
       JsonFieldAccessor writeAccessor,
       JsonFieldAccessor readAccessor,
       TypeRef<?> ownerType,
+      Type objectModelType,
       JsonCodec codecAnnotation,
       Class<? extends JsonValueCodec<?>> valueCodecClass,
       JsonFormat formatAnnotation,
@@ -165,9 +166,20 @@ public final class JsonFieldInfo {
     this.readSetter = readSetter;
     Class<?> writeFallback = writeRawType(writeField, writeGetter);
     Class<?> readFallback = readRawType(readField, readSetter);
-    this.writeType = resolveType(ownerType, writeType(writeField, writeGetter));
+    Type resolvedObjectModelType = resolveType(ownerType, objectModelType);
+    this.writeType =
+        writeFallback == null
+            ? null
+            : resolvedObjectModelType == null
+                ? resolveType(ownerType, writeType(writeField, writeGetter))
+                : resolvedObjectModelType;
     this.writeRawType = semanticRawType(writeType, writeFallback);
-    this.readType = resolveType(ownerType, readType(readField, readSetter));
+    this.readType =
+        readFallback == null
+            ? null
+            : resolvedObjectModelType == null
+                ? resolveType(ownerType, readType(readField, readSetter))
+                : resolvedObjectModelType;
     this.readRawType = semanticRawType(readType, readFallback);
     this.codecAnnotation = codecAnnotation;
     this.valueCodecClass = valueCodecClass;
@@ -275,6 +287,7 @@ public final class JsonFieldInfo {
             writeAccessor,
             readAccessor,
             ownerType,
+            writeType != null ? writeType : readType,
             codecAnnotation,
             valueCodecClass,
             formatAnnotation,
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
index e2166e8e1..6ade7bacf 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
@@ -990,7 +990,7 @@ public final class JsonSharedRegistry {
       return new ScalarCodecs.EnumCodec(rawType);
     }
     if (rawType.isArray()) {
-      return ArrayCodec.create(rawType, localResolver);
+      return ArrayCodec.create(rawType, typeRef, localResolver);
     }
     if (rawType == Optional.class) {
       return new ScalarCodecs.OptionalCodec(
diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
index 638255b57..cdc825d6c 100644
--- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
+++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java
@@ -19,6 +19,7 @@
 
 package org.apache.fory.json;
 
+import java.lang.annotation.Annotation;
 import java.lang.invoke.MethodHandle;
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Constructor;
@@ -86,6 +87,11 @@ final class ForyJsonGraalVMFeature implements Feature {
       "org.apache.fory.json.scala.ScalaJsonCodec";
   private static final String SCALA_JSON_CODEC_FACTORY =
       "org.apache.fory.json.scala.internal.ScalaTypeCodecFactory$";
+  private static final String SCALA_ENUMERATION_ANNOTATION =
+      "org.apache.fory.json.scala.JsonEnumeration";
+  private static final String[] SCALA_ENUMERATION_SLOTS = {
+    "value", "element", "content", "mapKey", "mapValue"
+  };
   private static final String SCALA_ENUM_CLASS = "scala.reflect.Enum";
   private static final String[] SQL_TYPES = {
     "java.sql.Date", "java.sql.Time", "java.sql.Timestamp"
@@ -100,6 +106,7 @@ final class ForyJsonGraalVMFeature implements Feature {
   private final Set<Class<?>> processedProviders = ConcurrentHashMap.newKeySet();
   private final Set<Class<?>> processedFactoryModels = ConcurrentHashMap.newKeySet();
   private final Set<Class<?>> scalaDerivedModels = ConcurrentHashMap.newKeySet();
+  private final Set<Class<?>> scalaEnumerationOwners = ConcurrentHashMap.newKeySet();
   private final Set<Class<?>> processedCodecs = ConcurrentHashMap.newKeySet();
   private final Set<Class<?>> processedContainers = ConcurrentHashMap.newKeySet();
   private final Set<Executable> processedCreators = new LinkedHashSet<>();
@@ -581,6 +588,7 @@ final class ForyJsonGraalVMFeature implements Feature {
 
   private void registerModelHierarchy(
       DuringAnalysisAccess access, Class<?> type, JsonMixinView annotations) {
+    registerScalaEnumerationOwners(type);
     TypeRef<?> ownerType = TypeRef.of(type);
     boolean record = type.isRecord();
     for (Class<?> current = type;
@@ -661,6 +669,71 @@ final class ForyJsonGraalVMFeature implements Feature {
     }
   }
 
+  private void registerScalaEnumerationOwners(Class<?> type) {
+    for (Class<?> current = type;
+        current != null && current != Object.class;
+        current = current.getSuperclass()) {
+      for (Field field : current.getDeclaredFields()) {
+        registerScalaEnumerationOwner(field);
+      }
+      for (Method method : current.getDeclaredMethods()) {
+        registerScalaEnumerationOwner(method);
+        for (Parameter parameter : method.getParameters()) {
+          registerScalaEnumerationOwner(parameter);
+        }
+      }
+      for (Constructor<?> constructor : current.getDeclaredConstructors()) {
+        for (Parameter parameter : constructor.getParameters()) {
+          registerScalaEnumerationOwner(parameter);
+        }
+      }
+    }
+  }
+
+  private void registerScalaEnumerationOwner(AnnotatedElement element) {
+    for (Annotation annotation : element.getDeclaredAnnotations()) {
+      Class<? extends Annotation> annotationType = annotation.annotationType();
+      if (!annotationType.getName().equals(SCALA_ENUMERATION_ANNOTATION)) {
+        continue;
+      }
+      RuntimeReflection.register(annotationType);
+      for (String slot : SCALA_ENUMERATION_SLOTS) {
+        Method method;
+        Class<?> owner;
+        try {
+          method = annotationType.getMethod(slot);
+          owner = (Class<?>) method.invoke(annotation);
+        } catch (ReflectiveOperationException | ClassCastException e) {
+          throw new IllegalStateException("Invalid Scala JSON Enumeration annotation", e);
+        }
+        RuntimeReflection.register(method);
+        if (owner != Void.class && scalaEnumerationOwners.add(owner)) {
+          registerScalaEnumerationOwner(owner);
+        }
+      }
+    }
+  }
+
+  private static void registerScalaEnumerationOwner(Class<?> owner) {
+    RuntimeReflection.register(owner);
+    Field field;
+    try {
+      field = owner.getField("MODULE$");
+    } catch (NoSuchFieldException e) {
+      throw new IllegalStateException(
+          "Scala Enumeration owner has no public MODULE$ field: " + owner.getName(), e);
+    }
+    int modifiers = field.getModifiers();
+    if (field.getType() != owner
+        || !Modifier.isPublic(modifiers)
+        || !Modifier.isStatic(modifiers)
+        || !Modifier.isFinal(modifiers)) {
+      throw new IllegalStateException(
+          "Invalid Scala Enumeration owner singleton: " + owner.getName());
+    }
+    RuntimeReflection.register(field);
+  }
+
   private void registerRecord(Class<?> type) {
     RuntimeReflection.registerAllRecordComponents(type);
     for (RecordComponent component : type.getRecordComponents()) {
diff --git a/scala/build.sbt b/scala/build.sbt
index 374bce62c..6a648dd54 100644
--- a/scala/build.sbt
+++ b/scala/build.sbt
@@ -76,6 +76,7 @@ lazy val foryJsonScala = Project(id = "fory-json-scala", base = file("fory-json-
   .settings(commonSettings)
   .settings(
     name := "fory-json-scala",
+    Compile / javacOptions ++= Seq("--release", "8"),
     libraryDependencies ++= {
       val reflect =
         if (scalaBinaryVersion.value == "2.13")
diff --git a/scala/fory-json-scala/src/main/java/org/apache/fory/json/scala/JsonEnumeration.java b/scala/fory-json-scala/src/main/java/org/apache/fory/json/scala/JsonEnumeration.java
new file mode 100644
index 000000000..5cbdce07d
--- /dev/null
+++ b/scala/fory-json-scala/src/main/java/org/apache/fory/json/scala/JsonEnumeration.java
@@ -0,0 +1,53 @@
+/*
+ * 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.json.scala;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Binds direct Scala 2 {@code scala.Enumeration.Value} occurrences to their singleton owners.
+ *
+ * <p>The selected classes must be Scala {@code Enumeration} singleton classes such as the class
+ * literal produced by {@code classOf[Weekday.type]}. Child slots describe only the direct element,
+ * content, key, or value occurrence of the annotated property.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
+public @interface JsonEnumeration {
+  /** Owner of a directly declared {@code Enumeration.Value}. */
+  Class<?> value() default Void.class;
+
+  /** Owner of a direct Scala collection or array element. */
+  Class<?> element() default Void.class;
+
+  /** Owner of direct {@code Option} content. */
+  Class<?> content() default Void.class;
+
+  /** Owner of a direct Scala map key. */
+  Class<?> mapKey() default Void.class;
+
+  /** Owner of a direct Scala map value. */
+  Class<?> mapValue() default Void.class;
+}
diff --git a/scala/fory-json-scala/src/main/scala-2/org/apache/fory/json/scala/internal/ScalaEnumerationTypes.scala b/scala/fory-json-scala/src/main/scala-2/org/apache/fory/json/scala/internal/ScalaEnumerationTypes.scala
new file mode 100644
index 000000000..fad47cc0c
--- /dev/null
+++ b/scala/fory-json-scala/src/main/scala-2/org/apache/fory/json/scala/internal/ScalaEnumerationTypes.scala
@@ -0,0 +1,346 @@
+/*
+ * 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.json.scala.internal
+
+import java.lang.reflect.{AnnotatedElement, Constructor, GenericArrayType, Method, Modifier, ParameterizedType, Type}
+import java.util.{Arrays, Objects}
+
+import org.apache.fory.json.ForyJsonException
+import org.apache.fory.json.annotation.JsonCodec
+import org.apache.fory.json.codec.{JsonValueCodec, MapKeyCodec}
+import org.apache.fory.json.scala.{JsonEnumeration, ScalaEnumerationCodec}
+import org.apache.fory.reflect.TypeRef
+
+private[scala] object ScalaEnumerationTypes {
+  private val ValueClass = classOf[Enumeration#Value]
+
+  def propertyTypes(
+      typeClass: Class[_],
+      constructor: Constructor[_],
+      names: Array[String],
+      getters: Array[Method],
+      setters: Array[Method]
+  ): Array[Type] = {
+    val constructorParameters = constructor.getParameters
+    val constructorTypes = constructor.getGenericParameterTypes
+    val result = new Array[Type](names.length)
+    var index = 0
+    while (index < names.length) {
+      val getter = getters(index)
+      val setter = setters(index)
+      val declaredType =
+        if (index < constructorTypes.length) constructorTypes(index)
+        else if (getter != null) getter.getGenericReturnType
+        else setter.getGenericParameterTypes.apply(0)
+      val constructorParameter =
+        if (index < constructorParameters.length) constructorParameters(index) else null
+      result(index) = resolveProperty(
+        typeClass,
+        names(index),
+        declaredType,
+        constructorParameter,
+        getter,
+        setter
+      )
+      index += 1
+    }
+    result
+  }
+
+  def createCodec(typeRef: TypeRef[_]): JsonValueCodec[_] = {
+    val owner = enumerationOwner(typeRef.getType)
+    if (owner == null) null else new BoundEnumerationCodec(enumeration(owner))
+  }
+
+  def mapKeyCodec(keyType: Type): MapKeyCodec = {
+    val owner = enumerationOwner(keyType)
+    if (owner == null) null else new BoundEnumerationCodec(enumeration(owner))
+  }
+
+  private def resolveProperty(
+      typeClass: Class[_],
+      name: String,
+      declaredType: Type,
+      constructorParameter: AnnotatedElement,
+      getter: Method,
+      setter: Method
+  ): Type = {
+    var selected: JsonEnumeration = null
+    var selectedSource: AnnotatedElement = null
+    var codecSource: AnnotatedElement = null
+
+    def inspect(source: AnnotatedElement): Unit = {
+      if (source == null) return
+      if (source.getDeclaredAnnotation(classOf[JsonCodec]) != null) codecSource = source
+      val annotation = source.getDeclaredAnnotation(classOf[JsonEnumeration])
+      if (annotation != null) {
+        if (selected != null && !sameSelection(selected, annotation)) {
+          throw new ForyJsonException(
+            s"Conflicting @JsonEnumeration declarations for ${typeClass.getName}.$name from " +
+              s"$selectedSource and $source"
+          )
+        }
+        if (selected == null) {
+          selected = annotation
+          selectedSource = source
+        }
+      }
+    }
+
+    inspect(constructorParameter)
+    inspect(getter)
+    val fields = typeClass.getDeclaredFields
+    var fieldIndex = 0
+    while (fieldIndex < fields.length) {
+      if (fields(fieldIndex).getName == name) inspect(fields(fieldIndex))
+      fieldIndex += 1
+    }
+    inspect(setter)
+    if (setter != null && setter.getParameterCount == 1) inspect(setter.getParameters.apply(0))
+
+    if (selected == null) return declaredType
+    if (codecSource != null) {
+      throw new ForyJsonException(
+        s"@JsonEnumeration cannot coexist with @JsonCodec for ${typeClass.getName}.$name"
+      )
+    }
+    applySelection(declaredType, selected, s"${typeClass.getName}.$name")
+  }
+
+  private def applySelection(valueType: Type, annotation: JsonEnumeration, property: String): Type = {
+    val value = configured(annotation.value())
+    val element = configured(annotation.element())
+    val content = configured(annotation.content())
+    val mapKey = configured(annotation.mapKey())
+    val mapValue = configured(annotation.mapValue())
+    val childCount =
+      (if (element) 1 else 0) + (if (content) 1 else 0) +
+        (if (mapKey) 1 else 0) + (if (mapValue) 1 else 0)
+    if (value) {
+      if (childCount != 0)
+        throw invalid(property, "value cannot be combined with child slots")
+      requireValue(valueType, property)
+      return ownedValue(annotation.value(), property)
+    }
+    if (childCount == 0) throw invalid(property, "at least one owner slot is required")
+    if (element) {
+      if (childCount != 1) throw invalid(property, "element cannot be combined with other child slots")
+      return replaceElement(valueType, annotation.element(), property)
+    }
+    if (content) {
+      if (childCount != 1) throw invalid(property, "content cannot be combined with other child slots")
+      return replaceContent(valueType, annotation.content(), property)
+    }
+    replaceMap(valueType, annotation.mapKey(), annotation.mapValue(), property)
+  }
+
+  private def replaceElement(valueType: Type, owner: Class[_], property: String): Type =
+    valueType match {
+      case cls: Class[_] if cls.isArray =>
+        requireValue(cls.getComponentType, property)
+        new OwnedArrayType(ownedValue(owner, property))
+      case array: GenericArrayType =>
+        requireValue(array.getGenericComponentType, property)
+        new OwnedArrayType(ownedValue(owner, property))
+      case parameterized: ParameterizedType =>
+        val rawType = rawClass(parameterized)
+        if (
+          rawType == null || classOf[scala.collection.Map[_, _]].isAssignableFrom(rawType) ||
+          !classOf[scala.collection.Iterable[_]].isAssignableFrom(rawType)
+        ) throw invalid(property, "element requires a Scala collection or array")
+        val arguments = parameterized.getActualTypeArguments
+        if (arguments.length != 1) throw invalid(property, "element requires one direct type argument")
+        requireValue(arguments(0), property)
+        rebuild(parameterized, Array(ownedValue(owner, property)))
+      case _ => throw invalid(property, "element requires a Scala collection or array")
+    }
+
+  private def replaceContent(valueType: Type, owner: Class[_], property: String): Type =
+    valueType match {
+      case parameterized: ParameterizedType =>
+        val rawType = rawClass(parameterized)
+        if (rawType == null || !classOf[Option[_]].isAssignableFrom(rawType))
+          throw invalid(property, "content requires Option")
+        val arguments = parameterized.getActualTypeArguments
+        if (arguments.length != 1) throw invalid(property, "content requires one direct type argument")
+        requireValue(arguments(0), property)
+        rebuild(parameterized, Array(ownedValue(owner, property)))
+      case _ => throw invalid(property, "content requires a parameterized Option")
+    }
+
+  private def replaceMap(
+      valueType: Type,
+      keyOwner: Class[_],
+      valueOwner: Class[_],
+      property: String
+  ): Type = valueType match {
+    case parameterized: ParameterizedType =>
+      val rawType = rawClass(parameterized)
+      if (rawType == null || !classOf[scala.collection.Map[_, _]].isAssignableFrom(rawType))
+        throw invalid(property, "mapKey and mapValue require a Scala Map")
+      val arguments = parameterized.getActualTypeArguments
+      if (arguments.length != 2) throw invalid(property, "Scala Map requires key and value arguments")
+      val rewritten = arguments.clone()
+      if (configured(keyOwner)) {
+        requireValue(arguments(0), property)
+        rewritten(0) = ownedValue(keyOwner, property)
+      }
+      if (configured(valueOwner)) {
+        requireValue(arguments(1), property)
+        rewritten(1) = ownedValue(valueOwner, property)
+      }
+      rebuild(parameterized, rewritten)
+    case _ => throw invalid(property, "mapKey and mapValue require a parameterized Scala Map")
+  }
+
+  private def rebuild(valueType: ParameterizedType, arguments: Array[Type]): Type = {
+    new ResolvedParameterizedType(valueType.getOwnerType, valueType.getRawType, arguments)
+  }
+
+  private def ownedValue(owner: Class[_], property: String): Type = {
+    validateOwner(owner, property)
+    new ResolvedParameterizedType(owner, ValueClass, Array.empty)
+  }
+
+  private def enumerationOwner(valueType: Type): Class[_] = valueType match {
+    case parameterized: ParameterizedType
+        if rawClass(parameterized) == ValueClass &&
+          parameterized.getActualTypeArguments.length == 0 =>
+      val owner = rawClass(parameterized.getOwnerType)
+      if (owner == null) null
+      else {
+        validateOwner(owner, valueType.toString)
+        owner
+      }
+    case _ => null
+  }
+
+  private def validateOwner(owner: Class[_], source: String): Unit = {
+    if (!classOf[Enumeration].isAssignableFrom(owner))
+      throw invalid(source, s"${owner.getName} is not a Scala Enumeration singleton class")
+    val field =
+      try owner.getField("MODULE$")
+      catch {
+        case _: NoSuchFieldException =>
+          throw invalid(source, s"${owner.getName} has no public MODULE$$ field")
+      }
+    val modifiers = field.getModifiers
+    if (
+      field.getType != owner || !Modifier.isPublic(modifiers) ||
+      !Modifier.isStatic(modifiers) || !Modifier.isFinal(modifiers)
+    ) throw invalid(source, s"${owner.getName}.MODULE$$ is not a public static final singleton")
+  }
+
+  private def enumeration(owner: Class[_]): Enumeration = {
+    validateOwner(owner, owner.getName)
+    try {
+      val value = owner.getField("MODULE$").get(null)
+      value match {
+        case enumeration: Enumeration => enumeration
+        case _ => throw invalid(owner.getName, "MODULE$ is not a Scala Enumeration")
+      }
+    } catch {
+      case error: ReflectiveOperationException =>
+        throw new ForyJsonException(s"Cannot load Scala Enumeration owner ${owner.getName}", error)
+    }
+  }
+
+  private def requireValue(valueType: Type, property: String): Unit = {
+    if (rawClass(valueType) != ValueClass)
+      throw invalid(property, s"configured occurrence is ${valueType.getTypeName}, not Enumeration.Value")
+  }
+
+  private def rawClass(valueType: Type): Class[_] = valueType match {
+    case cls: Class[_]                  => cls
+    case parameterized: ParameterizedType => parameterized.getRawType match {
+        case cls: Class[_] => cls
+        case _             => null
+      }
+    case _ => null
+  }
+
+  private def configured(owner: Class[_]): Boolean = owner != classOf[Void]
+
+  private def sameSelection(left: JsonEnumeration, right: JsonEnumeration): Boolean =
+    left.value() == right.value() && left.element() == right.element() &&
+      left.content() == right.content() && left.mapKey() == right.mapKey() &&
+      left.mapValue() == right.mapValue()
+
+  private def invalid(property: String, reason: String): ForyJsonException =
+    new ForyJsonException(s"Invalid @JsonEnumeration on $property: $reason")
+
+  private final class BoundEnumerationCodec(enumeration: Enumeration)
+      extends ScalaEnumerationCodec(enumeration)
+
+  private final class OwnedArrayType(componentType: Type) extends GenericArrayType {
+    override def getGenericComponentType: Type = componentType
+
+    override def equals(other: Any): Boolean = other match {
+      case array: GenericArrayType => componentType == array.getGenericComponentType
+      case _                       => false
+    }
+
+    override def hashCode(): Int = componentType.hashCode()
+
+    override def toString: String = componentType.getTypeName + "[]"
+  }
+
+  private final class ResolvedParameterizedType(
+      ownerType: Type,
+      rawType: Type,
+      suppliedArguments: Array[Type]
+  ) extends ParameterizedType {
+    private val arguments = suppliedArguments.clone()
+
+    override def getOwnerType: Type = ownerType
+
+    override def getRawType: Type = rawType
+
+    override def getActualTypeArguments: Array[Type] = arguments.clone()
+
+    override def equals(other: Any): Boolean = other match {
+      case parameterized: ParameterizedType =>
+        Objects.equals(ownerType, parameterized.getOwnerType) &&
+          Objects.equals(rawType, parameterized.getRawType) &&
+          Arrays.equals(arguments.asInstanceOf[Array[Object]],
+            parameterized.getActualTypeArguments.asInstanceOf[Array[Object]])
+      case _ => false
+    }
+
+    override def hashCode(): Int =
+      Arrays.hashCode(arguments.asInstanceOf[Array[Object]]) ^ Objects.hashCode(ownerType) ^
+        Objects.hashCode(rawType)
+
+    override def toString: String = {
+      val builder = new StringBuilder(rawType.getTypeName)
+      if (arguments.nonEmpty) {
+        builder.append('<')
+        var index = 0
+        while (index < arguments.length) {
+          if (index != 0) builder.append(", ")
+          builder.append(arguments(index).getTypeName)
+          index += 1
+        }
+        builder.append('>')
+      }
+      builder.toString()
+    }
+  }
+}
diff --git a/scala/fory-json-scala/src/main/scala-3/org/apache/fory/json/scala/internal/ScalaEnumerationTypes.scala b/scala/fory-json-scala/src/main/scala-3/org/apache/fory/json/scala/internal/ScalaEnumerationTypes.scala
new file mode 100644
index 000000000..42bde6253
--- /dev/null
+++ b/scala/fory-json-scala/src/main/scala-3/org/apache/fory/json/scala/internal/ScalaEnumerationTypes.scala
@@ -0,0 +1,71 @@
+/*
+ * 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.json.scala.internal
+
+import java.lang.reflect.{Constructor, Method, Type}
+
+import org.apache.fory.json.ForyJsonException
+import org.apache.fory.json.codec.{JsonValueCodec, MapKeyCodec}
+import org.apache.fory.json.scala.JsonEnumeration
+import org.apache.fory.reflect.TypeRef
+
+private[scala] object ScalaEnumerationTypes {
+  def propertyTypes(
+      typeClass: Class[?],
+      constructor: Constructor[?],
+      names: Array[String],
+      getters: Array[Method],
+      setters: Array[Method]
+  ): Array[Type] = {
+    val constructorParameters = constructor.getParameters
+    val constructorTypes = constructor.getGenericParameterTypes
+    val types = new Array[Type](names.length)
+    var index = 0
+    while index < names.length do
+      val getter = getters(index)
+      val setter = setters(index)
+      val parameter = if index < constructorParameters.length then constructorParameters(index) else null
+      if annotation(parameter) || annotation(getter) || annotation(setter) then
+        throw new ForyJsonException(
+          s"@JsonEnumeration supports only Scala 2 Enumeration: ${typeClass.getName}.${names(index)}"
+        )
+      val fields = typeClass.getDeclaredFields
+      var fieldIndex = 0
+      while fieldIndex < fields.length do
+        if fields(fieldIndex).getName == names(index) && annotation(fields(fieldIndex)) then
+          throw new ForyJsonException(
+            s"@JsonEnumeration supports only Scala 2 Enumeration: ${typeClass.getName}.${names(index)}"
+          )
+        fieldIndex += 1
+      types(index) =
+        if index < constructorTypes.length then constructorTypes(index)
+        else if getter != null then getter.getGenericReturnType
+        else setter.getGenericParameterTypes.apply(0)
+      index += 1
+    types
+  }
+
+  def createCodec(typeRef: TypeRef[?]): JsonValueCodec[?] = null
+
+  def mapKeyCodec(keyType: Type): MapKeyCodec = null
+
+  private def annotation(element: java.lang.reflect.AnnotatedElement): Boolean =
+    element != null && element.getDeclaredAnnotation(classOf[JsonEnumeration]) != null
+}
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaCollectionCodecs.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaCollectionCodecs.scala
index 2e071f5b4..18e954747 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaCollectionCodecs.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaCollectionCodecs.scala
@@ -365,8 +365,12 @@ private[scala] final class ScalaMapCodec(kind: Int, ownerBytes: Int)
       "Scala Map",
       resolver.resolvingRuntimeType()
     )
-    val keyRawType = if (specializedKey == null) ScalaTypeSupport.rawType(arguments(0)) else specializedKey
-    keyCodec = resolver.getMapKeyCodec(keyRawType)
+    val keyType = if (specializedKey == null) arguments(0) else specializedKey
+    val keyRawType = ScalaTypeSupport.rawType(keyType)
+    val enumerationKeyCodec = ScalaEnumerationTypes.mapKeyCodec(keyType)
+    keyCodec =
+      if (enumerationKeyCodec == null) resolver.getMapKeyCodec(keyRawType)
+      else enumerationKeyCodec
     val valueType = arguments(if (specializedKey == null) 1 else 0)
     valueInfo = resolver.getTypeInfo(valueType, ScalaTypeSupport.rawType(valueType))
   }
@@ -386,8 +390,13 @@ private[scala] final class ScalaMapCodec(kind: Int, ownerBytes: Int)
     val keyType = if (specializedKey == null) arguments(0) else specializedKey
     val keyRawType = ScalaTypeSupport.rawType(keyType)
     keyCodec =
-      if (childCodecs.keyCodec() == classOf[JsonCodec.NoMapKeyCodec]) resolver.getMapKeyCodec(keyRawType)
-      else resolver.getMapKeyCodec(keyRawType, childCodecs.keyCodec())
+      if (childCodecs.keyCodec() != classOf[JsonCodec.NoMapKeyCodec])
+        resolver.getMapKeyCodec(keyRawType, childCodecs.keyCodec())
+      else {
+        val enumerationKeyCodec = ScalaEnumerationTypes.mapKeyCodec(keyType)
+        if (enumerationKeyCodec == null) resolver.getMapKeyCodec(keyRawType)
+        else enumerationKeyCodec
+      }
     val valueType = arguments(if (specializedKey == null) 1 else 0)
     val valueRawType = ScalaTypeSupport.rawType(valueType)
     valueInfo =
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaObjectModels.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaObjectModels.scala
index 45b57d0f0..56ae2265a 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaObjectModels.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaObjectModels.scala
@@ -69,6 +69,14 @@ private[scala] object ScalaObjectModels {
     val propertySetters =
       Array.fill[Method](names.length)(null) ++
         bodyPropertyIndexes.map(index => propertySetter(typeClass, bodyFields(index)))
+    val propertyTypes =
+      ScalaEnumerationTypes.propertyTypes(
+        typeClass,
+        constructor,
+        propertyNames,
+        propertyGetters,
+        propertySetters
+      )
     val defaults = constructorDefaults(typeClass, parameterTypes)
     resolver.createObjectCodec(
       typeRef,
@@ -79,7 +87,8 @@ private[scala] object ScalaObjectModels {
         defaults,
         propertyNames,
         propertyGetters,
-        propertySetters
+        propertySetters,
+        propertyTypes
       )
     )
   }
diff --git a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTypeCodecFactory.scala b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTypeCodecFactory.scala
index d3549169b..487fb34fc 100644
--- a/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTypeCodecFactory.scala
+++ b/scala/fory-json-scala/src/main/scala/org/apache/fory/json/scala/internal/ScalaTypeCodecFactory.scala
@@ -33,6 +33,8 @@ private[scala] object ScalaTypeCodecFactory extends JsonCodecFactory {
   override def create(typeRef: TypeRef[_], resolver: JsonTypeResolver): JsonValueCodec[_] = {
     val rawType = typeRef.getRawType
     val name = rawType.getName
+    val enumerationCodec = ScalaEnumerationTypes.createCodec(typeRef)
+    if (enumerationCodec != null) return enumerationCodec
 
     if (isRejected(rawType)) throw ScalaTypeSupport.unsupported(typeRef, "runtime-state or lazy type")
     if (classOf[Range].isAssignableFrom(rawType)) {
diff --git a/scala/fory-json-scala/src/test/scala-2/org/apache/fory/json/scala/ScalaJsonEnumerationNativeImageMain.scala b/scala/fory-json-scala/src/test/scala-2/org/apache/fory/json/scala/ScalaJsonEnumerationNativeImageMain.scala
new file mode 100644
index 000000000..aa9abd216
--- /dev/null
+++ b/scala/fory-json-scala/src/test/scala-2/org/apache/fory/json/scala/ScalaJsonEnumerationNativeImageMain.scala
@@ -0,0 +1,50 @@
+/*
+ * 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.json.scala
+
+import org.apache.fory.json.ForyJson
+import org.apache.fory.json.annotation.{ForyJsonProvider, JsonType}
+
+object NativeWeekday extends Enumeration {
+  val Monday, Tuesday = Value
+}
+
+@JsonType
+case class NativeEnumerationSchedule(
+    @JsonEnumeration(classOf[NativeWeekday.type]) day: NativeWeekday.Value,
+    @JsonEnumeration(element = classOf[NativeWeekday.type]) days: List[NativeWeekday.Value]
+)
+
+@ForyJsonProvider
+final class ScalaEnumerationNativeConfig {
+  def json(): ForyJson = ForyJsonScala.builder().build()
+}
+
+object ScalaJsonEnumerationNativeImageMain {
+  def main(args: Array[String]): Unit = {
+    val json = new ScalaEnumerationNativeConfig().json()
+    val value = NativeEnumerationSchedule(
+      NativeWeekday.Tuesday,
+      List(NativeWeekday.Monday, NativeWeekday.Tuesday)
+    )
+    require(json.fromJson(json.toJson(value), classOf[NativeEnumerationSchedule]) == value)
+    println("Fory Scala 2 Enumeration native image succeeded")
+  }
+}
diff --git a/scala/fory-json-scala/src/test/scala-2/org/apache/fory/json/scala/ScalaJsonEnumerationSuite.scala b/scala/fory-json-scala/src/test/scala-2/org/apache/fory/json/scala/ScalaJsonEnumerationSuite.scala
new file mode 100644
index 000000000..696e4557e
--- /dev/null
+++ b/scala/fory-json-scala/src/test/scala-2/org/apache/fory/json/scala/ScalaJsonEnumerationSuite.scala
@@ -0,0 +1,145 @@
+/*
+ * 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.json.scala
+
+import org.apache.fory.json.ForyJsonException
+import org.apache.fory.json.annotation.{JsonCodec, JsonProperty}
+import org.scalatest.funsuite.AnyFunSuite
+
+import scala.annotation.meta.{field, getter, param}
+
+object AnnotatedWeekday extends Enumeration {
+  val Monday, Tuesday = Value
+}
+
+object AnnotatedMonth extends Enumeration {
+  val January, February = Value
+}
+
+final class AnnotatedWeekdayCodec extends ScalaEnumerationCodec(AnnotatedWeekday)
+
+case class AnnotatedSchedule(
+    @JsonEnumeration(classOf[AnnotatedWeekday.type]) day: AnnotatedWeekday.Value,
+    @JsonEnumeration(element = classOf[AnnotatedWeekday.type]) days: List[AnnotatedWeekday.Value],
+    @JsonEnumeration(content = classOf[AnnotatedMonth.type]) month: Option[AnnotatedMonth.Value],
+    @JsonEnumeration(
+      mapKey = classOf[AnnotatedWeekday.type],
+      mapValue = classOf[AnnotatedMonth.type]
+    ) labels: Map[AnnotatedWeekday.Value, AnnotatedMonth.Value]
+)
+
+case class AnnotatedArray(
+    @JsonEnumeration(element = classOf[AnnotatedWeekday.type]) values: Array[AnnotatedWeekday.Value]
+)
+
+case class NullableAnnotatedValue(
+    @JsonEnumeration(classOf[AnnotatedWeekday.type])
+    @JsonProperty(include = JsonProperty.Include.ALWAYS)
+    value: AnnotatedWeekday.Value
+)
+
+case class InvalidEnumerationSlots(
+    @JsonEnumeration(
+      value = classOf[AnnotatedWeekday.type],
+      element = classOf[AnnotatedWeekday.type]
+    ) value: AnnotatedWeekday.Value
+)
+
+case class InvalidEnumerationShape(
+    @JsonEnumeration(element = classOf[AnnotatedWeekday.type]) value: String
+)
+
+case class ConflictingEnumerationCodec(
+    @JsonEnumeration(classOf[AnnotatedWeekday.type])
+    @JsonCodec(value = classOf[AnnotatedWeekdayCodec])
+    value: AnnotatedWeekday.Value
+)
+
+case class ConflictingEnumerationOwners(
+    @(JsonEnumeration @param)(classOf[AnnotatedWeekday.type])
+    @(JsonEnumeration @getter)(classOf[AnnotatedMonth.type])
+    value: AnnotatedWeekday.Value
+)
+
+case class FieldEnumeration(
+    @(JsonEnumeration @field)(classOf[AnnotatedWeekday.type]) value: AnnotatedWeekday.Value
+)
+
+class ScalaJsonEnumerationSuite extends AnyFunSuite {
+  private def jsonInstances = Seq(
+    ForyJsonScala.builder().withCodegen(false).build(),
+    ForyJsonScala.builder().withAsyncCompilation(false).build()
+  )
+
+  test("annotation binds direct and composite values") {
+    val value = AnnotatedSchedule(
+      AnnotatedWeekday.Tuesday,
+      List(AnnotatedWeekday.Monday, AnnotatedWeekday.Tuesday),
+      Some(AnnotatedMonth.February),
+      Map(AnnotatedWeekday.Monday -> AnnotatedMonth.January)
+    )
+    for (json <- jsonInstances) {
+      val encoded = json.toJson(value)
+      assert(encoded.contains("\"day\":\"Tuesday\""))
+      assert(encoded.contains("\"days\":[\"Monday\",\"Tuesday\"]"))
+      assert(encoded.contains("\"month\":\"February\""))
+      assert(encoded.contains("\"Monday\":\"January\""))
+      assert(json.fromJson(encoded, classOf[AnnotatedSchedule]) == value)
+    }
+  }
+
+  test("annotation binds array elements and field declarations") {
+    for (json <- jsonInstances) {
+      val array = AnnotatedArray(Array(AnnotatedWeekday.Monday, AnnotatedWeekday.Tuesday))
+      val decoded = json.fromJson(json.toJson(array), classOf[AnnotatedArray])
+      assert(decoded.values.toSeq == array.values.toSeq)
+
+      val field = FieldEnumeration(AnnotatedWeekday.Tuesday)
+      assert(json.fromJson(json.toJson(field), classOf[FieldEnumeration]) == field)
+    }
+  }
+
+  test("annotation preserves owner and null rules") {
+    for (json <- jsonInstances) {
+      assert(
+        json.fromJson("{\"value\":null}", classOf[NullableAnnotatedValue]) ==
+          NullableAnnotatedValue(null)
+      )
+      assert(json.toJson(NullableAnnotatedValue(null)) == "{\"value\":null}")
+      assertThrows[ForyJsonException] {
+        json.fromJson("{\"value\":\"Friday\"}", classOf[NullableAnnotatedValue])
+      }
+      val wrongOwner = AnnotatedMonth.January.asInstanceOf[AnnotatedWeekday.Value]
+      assertThrows[ForyJsonException](json.toJson(NullableAnnotatedValue(wrongOwner)))
+    }
+  }
+
+  test("annotation rejects invalid declarations") {
+    val json = ForyJsonScala.builder().withCodegen(false).build()
+    assertThrows[ForyJsonException](json.toJson(InvalidEnumerationSlots(AnnotatedWeekday.Monday)))
+    assertThrows[ForyJsonException](json.toJson(InvalidEnumerationShape("Monday")))
+    assertThrows[ForyJsonException] {
+      json.toJson(ConflictingEnumerationCodec(AnnotatedWeekday.Monday))
+    }
+    assertThrows[ForyJsonException] {
+      json.toJson(ConflictingEnumerationOwners(AnnotatedWeekday.Monday))
+    }
+  }
+}


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