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