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 6735b3788 feat: make generate enum use id for wire format (#3576)
6735b3788 is described below

commit 6735b37889ac904ec014298ee7ef4fd470c84823
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Apr 16 14:06:05 2026 +0800

    feat: make generate enum use id for wire format (#3576)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    make generate enum use id for wire format
    - java
    - python
    - c++
    - swift
    - javascript
    
    ## 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 of the final clean AI review results
    from both fresh reviewers 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
---
 .github/workflows/release-javascript.yaml          | 140 +++++++++++++++++++++
 ci/release.py                                      |   2 +-
 compiler/fory_compiler/generators/java.py          |  48 ++++---
 .../fory_compiler/tests/test_generated_code.py     |  26 ++++
 .../fory_compiler/tests/test_package_options.py    |  12 ++
 cpp/fory/serialization/enum_serializer.h           |  62 ++++++++-
 cpp/fory/serialization/serialization_test.cc       |  31 +++++
 integration_tests/idl_tests/idl/auto_id.fdl        |   4 +-
 .../idl_tests/javascript/test/roundtrip.test.ts    |   4 +-
 javascript/packages/core/lib/gen/enum.ts           |  47 +++++--
 javascript/test/enum.test.ts                       |  13 +-
 python/pyfory/_serializer.py                       |  28 ++++-
 python/pyfory/serialization.pyx                    |  40 +++++-
 python/pyfory/tests/test_serializer.py             |  14 ++-
 swift/Sources/ForyMacro/ForyObjectMacro.swift      |  47 ++++++-
 swift/Tests/ForyTests/ForySwiftTests.swift         |  23 ++++
 16 files changed, 496 insertions(+), 45 deletions(-)

diff --git a/.github/workflows/release-javascript.yaml 
b/.github/workflows/release-javascript.yaml
new file mode 100644
index 000000000..359c1649e
--- /dev/null
+++ b/.github/workflows/release-javascript.yaml
@@ -0,0 +1,140 @@
+# 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.
+
+name: Publish JavaScript
+run-name: "JavaScript Release: ${{ github.ref_name }}"
+
+on:
+  push:
+    tags: ['v*']
+
+permissions:
+  contents: read
+  id-token: write
+
+concurrency:
+  group: release-javascript-${{ github.ref }}
+  cancel-in-progress: false
+
+jobs:
+  publish-npm:
+    runs-on: ubuntu-latest
+    if: github.repository == 'apache/fory'
+    steps:
+      - uses: actions/checkout@v5
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+          registry-url: 'https://registry.npmjs.org'
+
+      - name: Upgrade npm for trusted publishing
+        run: npm i -g npm@latest
+
+      - name: Verify Node/npm versions
+        shell: bash
+        run: |
+          set -euo pipefail
+          echo "Node: $(node --version)"
+          echo "npm: $(npm --version)"
+          REQUIRED="11.5.1"
+          CURRENT="$(npm --version)"
+          if [[ "$(printf '%s\n' "$REQUIRED" "$CURRENT" | sort -V | head -n1)" 
!= "$REQUIRED" ]]; then
+            echo "npm version must be >= $REQUIRED for trusted publishing. 
current=$CURRENT"
+            exit 1
+          fi
+
+      - name: Bump javascript version
+        shell: bash
+        run: |
+          set -euo pipefail
+          VERSION="${{ github.ref_name }}"
+          VERSION="${VERSION#v}"
+          python ci/release.py bump_version -l javascript -version "$VERSION"
+
+      - name: Install dependencies
+        shell: bash
+        working-directory: javascript
+        run: npm install
+
+      - name: Build packages
+        shell: bash
+        working-directory: javascript
+        run: npm run build
+
+      - name: Prepare npm for trusted publishing
+        shell: bash
+        run: |
+          set -euo pipefail
+          npm config set registry https://registry.npmjs.org/
+          npm config delete //registry.npmjs.org/:_authToken || true
+          npm config get registry
+
+      - name: Verify repository metadata for provenance
+        shell: bash
+        run: |
+          set -euo pipefail
+          node <<'EOF'
+          const fs = require("node:fs");
+
+          const expected = "[email protected]:apache/fory.git";
+          const files = [
+            "javascript/packages/core/package.json",
+            "javascript/packages/hps/package.json"
+          ];
+
+          for (const file of files) {
+            const json = JSON.parse(fs.readFileSync(file, "utf8"));
+            const actual = json?.repository ?? "";
+            if (actual !== expected) {
+              console.error(
+                `Repository URL mismatch in ${file}: expected "${expected}", 
got "${actual}"`
+              );
+              process.exit(1);
+            }
+          }
+
+          console.log("repository metadata verified for all publishable 
packages");
+          EOF
+
+      - name: Publish @apache-fory/hps
+        shell: bash
+        working-directory: javascript/packages/hps
+        run: |
+          set -euo pipefail
+          TAG="latest"
+          if echo "${{ github.ref_name }}" | grep -qE '[-]'; then
+            TAG="next"
+          fi
+          npm publish --access public --tag "$TAG" --provenance
+        env:
+          NODE_AUTH_TOKEN: ""
+          NPM_TOKEN: ""
+
+      - name: Publish @apache-fory/core
+        shell: bash
+        working-directory: javascript/packages/core
+        run: |
+          set -euo pipefail
+          TAG="latest"
+          if echo "${{ github.ref_name }}" | grep -qE '[-]'; then
+            TAG="next"
+          fi
+          npm publish --access public --tag "$TAG" --provenance
+        env:
+          NODE_AUTH_TOKEN: ""
+          NPM_TOKEN: ""
diff --git a/ci/release.py b/ci/release.py
index 0caf8aaa0..b919b3344 100644
--- a/ci/release.py
+++ b/ci/release.py
@@ -177,7 +177,7 @@ def bump_version(**kwargs):
             bump_python_version(new_version)
         elif lang == "javascript":
             _bump_version(
-                "javascript/packages/fory",
+                "javascript/packages/core",
                 "package.json",
                 _normalize_js_version(new_version),
                 _update_js_version,
diff --git a/compiler/fory_compiler/generators/java.py 
b/compiler/fory_compiler/generators/java.py
index 8de256494..ee1d741bd 100644
--- a/compiler/fory_compiler/generators/java.py
+++ b/compiler/fory_compiler/generators/java.py
@@ -366,18 +366,14 @@ class JavaGenerator(BaseGenerator):
             lines.append(f"package {java_package};")
             lines.append("")
 
-        # Enum declaration
-        lines.append(f"public enum {enum.name} {{")
-
-        # Enum values (strip prefix for scoped enums)
-        for i, value in enumerate(enum.values):
-            comma = "," if i < len(enum.values) - 1 else ";"
-            stripped_name = self.strip_enum_prefix(enum.name, value.name)
-            lines.append(f"    {stripped_name}{comma}")
-
-        lines.append("}")
+        lines.append("import org.apache.fory.annotation.ForyEnumId;")
         lines.append("")
 
+        # Enum declaration
+        lines.extend(
+            self.generate_enum_declaration(enum, f"public enum {enum.name} {{")
+        )
+
         # Build file path
         path = self.get_java_package_path()
         if path:
@@ -531,7 +527,7 @@ class JavaGenerator(BaseGenerator):
         for message in self.schema.messages:
             self.collect_message_imports(message, imports)
         for enum in self.schema.enums:
-            pass  # Enums don't need special imports
+            self.collect_enum_imports(imports)
         for union in self.schema.unions:
             self.collect_union_imports(union, imports)
 
@@ -604,12 +600,19 @@ class JavaGenerator(BaseGenerator):
         if self.has_array_field_recursive(message):
             imports.add("java.util.Arrays")
 
+        for nested_enum in message.nested_enums:
+            self.collect_enum_imports(imports)
+
         # Collect imports from nested messages
         for nested_msg in message.nested_messages:
             self.collect_message_imports(nested_msg, imports)
         for nested_union in message.nested_unions:
             self.collect_union_imports(nested_union, imports)
 
+    def collect_enum_imports(self, imports: Set[str]):
+        """Collect imports required by generated Java enums."""
+        imports.add("org.apache.fory.annotation.ForyEnumId")
+
     def collect_union_imports(self, union: Union, imports: Set[str]):
         """Collect imports for a union and its cases."""
         imports.add("org.apache.fory.type.union.Union")
@@ -635,15 +638,30 @@ class JavaGenerator(BaseGenerator):
 
     def generate_nested_enum(self, enum: Enum) -> List[str]:
         """Generate a nested enum as a static inner class."""
-        lines = []
-        lines.append(f"public static enum {enum.name} {{")
+        return self.generate_enum_declaration(
+            enum, f"public static enum {enum.name} {{"
+        )
+
+    def generate_enum_declaration(self, enum: Enum, header: str) -> List[str]:
+        """Generate a Java enum declaration with stable Fory enum ids."""
+        lines = [header]
 
-        # Enum values (strip prefix for scoped enums)
         for i, value in enumerate(enum.values):
             comma = "," if i < len(enum.values) - 1 else ";"
             stripped_name = self.strip_enum_prefix(enum.name, value.name)
-            lines.append(f"    {stripped_name}{comma}")
+            lines.append(f"    {stripped_name}({value.value}){comma}")
 
+        lines.append("")
+        lines.append("    private final int id;")
+        lines.append("")
+        lines.append(f"    {enum.name}(int id) {{")
+        lines.append("        this.id = id;")
+        lines.append("    }")
+        lines.append("")
+        lines.append("    @ForyEnumId")
+        lines.append("    public int getId() {")
+        lines.append("        return id;")
+        lines.append("    }")
         lines.append("}")
         lines.append("")
         return lines
diff --git a/compiler/fory_compiler/tests/test_generated_code.py 
b/compiler/fory_compiler/tests/test_generated_code.py
index 34790883a..4385e07ed 100644
--- a/compiler/fory_compiler/tests/test_generated_code.py
+++ b/compiler/fory_compiler/tests/test_generated_code.py
@@ -543,6 +543,32 @@ def 
test_java_repeated_float16_generation_uses_float16_list():
     assert "private Float16List vals;" in java_output
 
 
+def test_java_enum_generation_uses_fory_enum_ids():
+    schema = parse_fdl(
+        dedent(
+            """
+            package gen;
+
+            enum Status {
+                UNKNOWN = 4096;
+                OK = 8192;
+            }
+            """
+        )
+    )
+    java_output = render_files(generate_files(schema, JavaGenerator))
+    assert "import org.apache.fory.annotation.ForyEnumId;" in java_output
+    assert "public enum Status {" in java_output
+    assert "UNKNOWN(4096)," in java_output
+    assert "OK(8192);" in java_output
+    assert "private final int id;" in java_output
+    assert "Status(int id) {" in java_output
+    assert "this.id = id;" in java_output
+    assert "@ForyEnumId" in java_output
+    assert "public int getId() {" in java_output
+    assert "return id;" in java_output
+
+
 def test_go_bfloat16_generation():
     idl = dedent(
         """
diff --git a/compiler/fory_compiler/tests/test_package_options.py 
b/compiler/fory_compiler/tests/test_package_options.py
index 89a09f35a..3bbf20d58 100644
--- a/compiler/fory_compiler/tests/test_package_options.py
+++ b/compiler/fory_compiler/tests/test_package_options.py
@@ -494,9 +494,15 @@ class TestJavaOuterClassname:
         assert "myapp/DescriptorProtos.java" == outer_file.path
 
         # Check content
+        assert "import org.apache.fory.annotation.ForyEnumId;" in 
outer_file.content
         assert "public final class DescriptorProtos" in outer_file.content
         assert "private DescriptorProtos()" in outer_file.content
         assert "public static enum Status" in outer_file.content
+        assert "UNKNOWN(0)," in outer_file.content
+        assert "ACTIVE(1);" in outer_file.content
+        assert "private final int id;" in outer_file.content
+        assert "@ForyEnumId" in outer_file.content
+        assert "public int getId()" in outer_file.content
         assert "public static class User" in outer_file.content
         assert "public static class Order" in outer_file.content
 
@@ -606,6 +612,12 @@ class TestJavaOuterClassname:
         assert "Status.java" in file_names
         assert "User.java" in file_names
         assert "MyappForyRegistration.java" in file_names
+        status_file = next(f for f in files if f.path.endswith("Status.java"))
+        assert "import org.apache.fory.annotation.ForyEnumId;" in 
status_file.content
+        assert "UNKNOWN(0);" in status_file.content
+        assert "private final int id;" in status_file.content
+        assert "@ForyEnumId" in status_file.content
+        assert "public int getId()" in status_file.content
 
 
 class TestJavaMultipleFiles:
diff --git a/cpp/fory/serialization/enum_serializer.h 
b/cpp/fory/serialization/enum_serializer.h
index ce125c412..f931764b8 100644
--- a/cpp/fory/serialization/enum_serializer.h
+++ b/cpp/fory/serialization/enum_serializer.h
@@ -37,14 +37,35 @@ namespace serialization {
 
 /// Serializer specialization for enum types.
 ///
-/// Writes the enum ordinal (underlying integral value) to match the xlang
-/// specification for value-based enums.
+/// Writes enum values using xlang-compatible uint32 wire ids. Registered enums
+/// with contiguous zero-based values keep ordinal encoding; enums with sparse
+/// non-negative declared values preserve those declared values on the wire.
 template <typename E>
 struct Serializer<E, std::enable_if_t<std::is_enum_v<E>>> {
   static constexpr TypeId type_id = TypeId::ENUM;
 
   using Metadata = meta::EnumMetadata<E>;
   using OrdinalType = typename Metadata::OrdinalType;
+  using EnumInfo = meta::EnumInfo<E>;
+
+  static constexpr bool supports_explicit_wire_values() {
+    if constexpr (!EnumInfo::defined) {
+      return true;
+    }
+    for (std::size_t i = 0; i < EnumInfo::size; ++i) {
+      const auto raw = static_cast<OrdinalType>(EnumInfo::values[i]);
+      if constexpr (std::is_signed_v<OrdinalType>) {
+        if (raw < 0) {
+          return false;
+        }
+      }
+      using Unsigned = std::make_unsigned_t<OrdinalType>;
+      if (static_cast<Unsigned>(raw) != static_cast<Unsigned>(i)) {
+        return true;
+      }
+    }
+    return false;
+  }
 
   static inline void write_type_info(WriteContext &ctx) {
     // Use compile-time type lookup for faster enum type info writing
@@ -72,6 +93,24 @@ struct Serializer<E, std::enable_if_t<std::is_enum_v<E>>> {
   }
 
   static inline void write_data(E value, WriteContext &ctx) {
+    if constexpr (supports_explicit_wire_values()) {
+      if constexpr (EnumInfo::defined) {
+        if (!EnumInfo::contains(value)) {
+          ctx.set_error(Error::unknown_enum("Unknown enum value"));
+          return;
+        }
+      }
+      const auto raw_value = static_cast<OrdinalType>(value);
+      if constexpr (std::is_signed_v<OrdinalType>) {
+        if (raw_value < 0) {
+          ctx.set_error(
+              Error::unknown_enum("Negative enum values are not supported"));
+          return;
+        }
+      }
+      ctx.write_var_uint32(static_cast<uint32_t>(raw_value));
+      return;
+    }
     OrdinalType ordinal{};
     if (!Metadata::to_ordinal(value, &ordinal)) {
       ctx.set_error(Error::unknown_enum("Unknown enum value"));
@@ -111,6 +150,25 @@ struct Serializer<E, std::enable_if_t<std::is_enum_v<E>>> {
     if (FORY_PREDICT_FALSE(ctx.has_error())) {
       return E{};
     }
+    if constexpr (supports_explicit_wire_values()) {
+      if (raw_ordinal >
+          static_cast<uint32_t>(std::numeric_limits<OrdinalType>::max())) {
+        ctx.set_error(Error::unknown_enum(
+            "Invalid enum value: " +
+            std::to_string(static_cast<unsigned long long>(raw_ordinal))));
+        return E{};
+      }
+      const auto value = static_cast<E>(static_cast<OrdinalType>(raw_ordinal));
+      if constexpr (EnumInfo::defined) {
+        if (!EnumInfo::contains(value)) {
+          ctx.set_error(Error::unknown_enum(
+              "Invalid enum value: " +
+              std::to_string(static_cast<unsigned long long>(raw_ordinal))));
+          return E{};
+        }
+      }
+      return value;
+    }
     OrdinalType ordinal = static_cast<OrdinalType>(raw_ordinal);
     E value{};
     if (!Metadata::from_ordinal(ordinal, &value)) {
diff --git a/cpp/fory/serialization/serialization_test.cc 
b/cpp/fory/serialization/serialization_test.cc
index 2f6b8ab63..d5ec5a86e 100644
--- a/cpp/fory/serialization/serialization_test.cc
+++ b/cpp/fory/serialization/serialization_test.cc
@@ -68,6 +68,8 @@ struct NestedStruct {
 enum class Color { RED, GREEN, BLUE };
 enum class LegacyStatus : int32_t { NEG = -3, ZERO = 0, LARGE = 42 };
 FORY_ENUM(LegacyStatus, NEG, ZERO, LARGE);
+enum class SparseStatus : int32_t { UNKNOWN = 4096, OK = 8192 };
+FORY_ENUM(SparseStatus, UNKNOWN, OK);
 
 enum OldStatus : int32_t { OLD_NEG = -7, OLD_ZERO = 0, OLD_POS = 13 };
 FORY_ENUM(::OldStatus, OLD_NEG, OLD_ZERO, OLD_POS);
@@ -92,6 +94,7 @@ inline void register_test_types(Fory &fory) {
   // Register all enum types used in tests
   fory.register_enum<Color>(type_id++);
   fory.register_enum<LegacyStatus>(type_id++);
+  fory.register_enum<SparseStatus>(type_id++);
   fory.register_enum<OldStatus>(type_id++);
 }
 
@@ -225,6 +228,11 @@ TEST(SerializationTest, OldEnumRoundtrip) {
   test_roundtrip(OldStatus::OLD_POS);
 }
 
+TEST(SerializationTest, SparseEnumRoundtrip) {
+  test_roundtrip(SparseStatus::UNKNOWN);
+  test_roundtrip(SparseStatus::OK);
+}
+
 TEST(SerializationTest, EnumSerializesOrdinalValue) {
   auto fory = Fory::builder().xlang(true).track_ref(false).build();
   fory.register_enum<LegacyStatus>(1);
@@ -308,6 +316,29 @@ TEST(SerializationTest, 
EnumOrdinalMappingRejectsInvalidOrdinal) {
   EXPECT_FALSE(decode.ok());
 }
 
+TEST(SerializationTest, SparseEnumSerializesExplicitValue) {
+  auto fory = Fory::builder().xlang(true).track_ref(false).build();
+  fory.register_enum<SparseStatus>(1);
+
+  auto bytes_result = fory.serialize(SparseStatus::OK);
+  ASSERT_TRUE(bytes_result.ok())
+      << "Serialization failed: " << bytes_result.error().to_string();
+
+  std::vector<uint8_t> bytes = bytes_result.value();
+  ASSERT_GE(bytes.size(), 1 + 1 + 1 + 1 + 2);
+  size_t offset = 1;
+  EXPECT_EQ(bytes[offset], static_cast<uint8_t>(NOT_NULL_VALUE_FLAG));
+  EXPECT_EQ(bytes[offset + 1], static_cast<uint8_t>(TypeId::ENUM));
+  EXPECT_EQ(bytes[offset + 2], 1);
+  EXPECT_EQ(bytes[offset + 3], 0x80);
+  EXPECT_EQ(bytes[offset + 4], 0x40);
+
+  auto roundtrip = fory.deserialize<SparseStatus>(bytes.data(), bytes.size());
+  ASSERT_TRUE(roundtrip.ok())
+      << "Deserialization failed: " << roundtrip.error().to_string();
+  EXPECT_EQ(roundtrip.value(), SparseStatus::OK);
+}
+
 TEST(SerializationTest, OldEnumOrdinalMappingHandlesNonZeroStart) {
   auto fory = Fory::builder().xlang(true).track_ref(false).build();
   fory.register_enum<OldStatus>(1);
diff --git a/integration_tests/idl_tests/idl/auto_id.fdl 
b/integration_tests/idl_tests/idl/auto_id.fdl
index f5a8a83ac..90946605d 100644
--- a/integration_tests/idl_tests/idl/auto_id.fdl
+++ b/integration_tests/idl_tests/idl/auto_id.fdl
@@ -18,8 +18,8 @@
 package auto_id;
 
 enum Status {
-    UNKNOWN = 0;
-    OK = 1;
+    UNKNOWN = 4096;
+    OK = 8192;
 }
 
 message Envelope {
diff --git a/integration_tests/idl_tests/javascript/test/roundtrip.test.ts 
b/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
index 0b8ba58d0..f21535024 100644
--- a/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
+++ b/integration_tests/idl_tests/javascript/test/roundtrip.test.ts
@@ -182,8 +182,8 @@ describe('Generated types compile and construct correctly', 
() => {
     expect(myEnvelope.payload?.value).toBe(42);
     expect(myEnvelope.status).toBe(Status.OK);
 
-    expect(Status.UNKNOWN).toBe(0);
-    expect(Status.OK).toBe(1);
+    expect(Status.UNKNOWN).toBe(4096);
+    expect(Status.OK).toBe(8192);
 
     expect(WrapperCase.ENVELOPE).toBe(1);
     expect(WrapperCase.RAW).toBe(2);
diff --git a/javascript/packages/core/lib/gen/enum.ts 
b/javascript/packages/core/lib/gen/enum.ts
index 20a9d91f2..22c70208b 100644
--- a/javascript/packages/core/lib/gen/enum.ts
+++ b/javascript/packages/core/lib/gen/enum.ts
@@ -32,15 +32,44 @@ class EnumSerializerGenerator extends 
BaseSerializerGenerator {
     this.typeInfo = typeInfo;
   }
 
+  private getEnumEntries(): Array<[string, string | number]> {
+    const enumProps = this.typeInfo.options?.enumProps;
+    if (!enumProps) {
+      return [];
+    }
+    return Object.entries(enumProps).filter(([key, value]) => {
+      return !(typeof value === "string" && Number.isInteger(Number(key)));
+    });
+  }
+
+  private useExplicitNumericWireValues(entries: Array<[string, string | 
number]>): boolean {
+    if (entries.length < 1) {
+      throw new Error("An enum must contain at least one field");
+    }
+    const seen = new Set<number>();
+    for (const [, value] of entries) {
+      if (typeof value === "string") {
+        return false;
+      }
+      if (!Number.isInteger(value) || value > MaxUInt32 || value < 0) {
+        throw new Error("Enum value must be a valid uint32");
+      }
+      if (seen.has(value)) {
+        throw new Error("Enum numeric values must be unique");
+      }
+      seen.add(value);
+    }
+    return true;
+  }
+
   write(accessor: string): string {
     if (!this.typeInfo.options?.enumProps) {
       return this.builder.writer.writeVarUInt32(accessor);
     }
-    if (Object.values(this.typeInfo.options.enumProps).length < 1) {
-      throw new Error("An enum must contain at least one field");
-    }
+    const enumEntries = this.getEnumEntries();
+    const useExplicitNumericWireValues = 
this.useExplicitNumericWireValues(enumEntries);
     return `
-        ${Object.values(this.typeInfo.options.enumProps).map((value, index) => 
{
+        ${enumEntries.map(([, value], index) => {
       if (typeof value !== "string" && typeof value !== "number") {
         throw new Error("Enum value must be string or number");
       }
@@ -50,8 +79,9 @@ class EnumSerializerGenerator extends BaseSerializerGenerator 
{
         }
       }
       const safeValue = typeof value === "string" ? `"${value}"` : value;
+      const wireValue = useExplicitNumericWireValues ? safeValue : index;
       return ` if (${accessor} === ${safeValue}) {
-                    ${this.builder.writer.writeVarUInt32(index)}
+                    ${this.builder.writer.writeVarUInt32(wireValue)}
                 }`;
     }).join(" else ")}
         else {
@@ -127,11 +157,13 @@ class EnumSerializerGenerator extends 
BaseSerializerGenerator {
     if (!this.typeInfo.options?.enumProps) {
       return accessor(this.builder.reader.readVarUInt32());
     }
+    const enumEntries = this.getEnumEntries();
+    const useExplicitNumericWireValues = 
this.useExplicitNumericWireValues(enumEntries);
     const enumValue = this.scope.uniqueName("enum_v");
     return `
         const ${enumValue} = ${this.builder.reader.readVarUInt32()};
         switch(${enumValue}) {
-            ${Object.values(this.typeInfo.options.enumProps).map((value, 
index) => {
+            ${enumEntries.map(([, value], index) => {
       if (typeof value !== "string" && typeof value !== "number") {
         throw new Error("Enum value must be string or number");
       }
@@ -141,8 +173,9 @@ class EnumSerializerGenerator extends 
BaseSerializerGenerator {
         }
       }
       const safeValue = typeof value === "string" ? `"${value}"` : `${value}`;
+      const wireValue = useExplicitNumericWireValues ? safeValue : `${index}`;
       return `
-                case ${index}:
+                case ${wireValue}:
                     ${accessor(safeValue)}
                     break;
                 `;
diff --git a/javascript/test/enum.test.ts b/javascript/test/enum.test.ts
index 4d5f72cb4..26aa5407d 100644
--- a/javascript/test/enum.test.ts
+++ b/javascript/test/enum.test.ts
@@ -62,6 +62,18 @@ describe('enum', () => {
     expect(result).toEqual(Foo.f1)
   });
 
+  test('should preserve sparse numeric enum values', () => {
+    const Foo = {
+      unknown: 4096,
+      ok: 8192
+    };
+    const fory = new Fory({ ref: true });
+    const { serialize, deserialize } = fory.register(Type.enum("example.foo", 
Foo));
+    const input = serialize(Foo.ok);
+    const result = deserialize(input);
+    expect(result).toEqual(Foo.ok);
+  });
+
   test('should typescript string enum work', () => {
     enum Foo {
         f1 = "hello",
@@ -76,4 +88,3 @@ describe('enum', () => {
     expect(result).toEqual(Foo.f1)
   });
 });
-
diff --git a/python/pyfory/_serializer.py b/python/pyfory/_serializer.py
index 7e46a653b..6b547cece 100644
--- a/python/pyfory/_serializer.py
+++ b/python/pyfory/_serializer.py
@@ -301,18 +301,38 @@ class EnumSerializer(Serializer):
         super().__init__(type_resolver, type_)
         self.need_to_write_ref = False
         self._members = tuple(type_)
-        self._ordinal_by_member = {member: idx for idx, member in 
enumerate(self._members)}
+        self._wire_value_by_member = {member: idx for idx, member in 
enumerate(self._members)}
+        self._member_by_wire_value = {idx: member for idx, member in 
enumerate(self._members)}
+        if type_resolver.xlang:
+            explicit_wire_values = {}
+            use_explicit_ids = True
+            for member in self._members:
+                raw_value = member.value
+                if isinstance(raw_value, bool) or not isinstance(raw_value, 
int) or raw_value < 0:
+                    use_explicit_ids = False
+                    break
+                wire_value = int(raw_value)
+                if wire_value in explicit_wire_values:
+                    use_explicit_ids = False
+                    break
+                explicit_wire_values[wire_value] = member
+            if use_explicit_ids:
+                self._wire_value_by_member = {member: int(member.value) for 
member in self._members}
+                self._member_by_wire_value = explicit_wire_values
 
     @classmethod
     def support_subclass(cls) -> bool:
         return True
 
     def write(self, write_context, value):
-        write_context.write_var_uint32(self._ordinal_by_member[value])
+        write_context.write_var_uint32(self._wire_value_by_member[value])
 
     def read(self, read_context):
-        ordinal = read_context.read_var_uint32()
-        return self._members[ordinal]
+        wire_value = read_context.read_var_uint32()
+        try:
+            return self._member_by_wire_value[wire_value]
+        except KeyError as exc:
+            raise ValueError(f"Unknown enum value {wire_value} for 
{self.type_.__qualname__}") from exc
 
 
 class SliceSerializer(Serializer):
diff --git a/python/pyfory/serialization.pyx b/python/pyfory/serialization.pyx
index e119d9fd8..0ccea8963 100644
--- a/python/pyfory/serialization.pyx
+++ b/python/pyfory/serialization.pyx
@@ -608,24 +608,54 @@ cdef class Serializer:
 @cython.final
 cdef class EnumSerializer(Serializer):
     cdef tuple _members
-    cdef dict _ordinal_by_member
+    cdef dict _wire_value_by_member
+    cdef dict _member_by_wire_value
 
     def __init__(self, TypeResolver type_resolver, type_):
+        cdef dict explicit_wire_values
+        cdef bint use_explicit_ids
+        cdef object member
+        cdef object raw_value
+        cdef uint32_t wire_value
         super().__init__(type_resolver, type_)
         self.need_to_write_ref = False
         self._members = tuple(type_)
-        self._ordinal_by_member = {member: idx for idx, member in 
enumerate(self._members)}
+        self._wire_value_by_member = {member: idx for idx, member in 
enumerate(self._members)}
+        self._member_by_wire_value = {idx: member for idx, member in 
enumerate(self._members)}
+        if type_resolver.xlang:
+            explicit_wire_values = {}
+            use_explicit_ids = True
+            for member in self._members:
+                raw_value = member.value
+                if isinstance(raw_value, bool) or not isinstance(raw_value, 
int) or raw_value < 0:
+                    use_explicit_ids = False
+                    break
+                wire_value = raw_value
+                if wire_value in explicit_wire_values:
+                    use_explicit_ids = False
+                    break
+                explicit_wire_values[wire_value] = member
+            if use_explicit_ids:
+                self._wire_value_by_member = {
+                    member: int(member.value) for member in self._members
+                }
+                self._member_by_wire_value = explicit_wire_values
 
     @classmethod
     def support_subclass(cls) -> bool:
         return True
 
     cpdef inline write(self, WriteContext write_context, value):
-        write_context.write_var_uint32(self._ordinal_by_member[value])
+        write_context.write_var_uint32(self._wire_value_by_member[value])
 
     cpdef inline read(self, ReadContext read_context):
-        cdef uint32_t ordinal = read_context.read_var_uint32()
-        return self._members[ordinal]
+        cdef uint32_t wire_value = read_context.read_var_uint32()
+        cdef object value = self._member_by_wire_value.get(wire_value)
+        if value is None:
+            raise ValueError(
+                f"Unknown enum value {wire_value} for 
{self.type_.__qualname__}"
+            )
+        return value
 
 
 @cython.final
diff --git a/python/pyfory/tests/test_serializer.py 
b/python/pyfory/tests/test_serializer.py
index 798772cb5..8fc963b8a 100644
--- a/python/pyfory/tests/test_serializer.py
+++ b/python/pyfory/tests/test_serializer.py
@@ -22,7 +22,7 @@ import io
 import os
 import pickle
 import weakref
-from enum import Enum
+from enum import Enum, IntEnum
 from typing import Any, List, Dict, Optional
 
 import numpy as np
@@ -503,6 +503,11 @@ class EnumClass(Enum):
     E4 = "E4"
 
 
+class SparseIntEnum(IntEnum):
+    A = 4096
+    B = 8192
+
+
 def test_enum():
     fory = Fory(xlang=False, ref=True)
     assert ser_de(fory, EnumClass.E1) == EnumClass.E1
@@ -512,6 +517,13 @@ def test_enum():
     assert isinstance(fory.type_resolver.get_serializer(EnumClass), 
EnumSerializer)
 
 
+def test_xlang_enum_uses_sparse_integer_values():
+    fory = Fory(xlang=True, ref=False)
+    fory.register_type(SparseIntEnum, type_id=301)
+    assert ser_de(fory, SparseIntEnum.A) == SparseIntEnum.A
+    assert ser_de(fory, SparseIntEnum.B) == SparseIntEnum.B
+
+
 def test_duplicate_serialize():
     fory = Fory(xlang=False, ref=True)
     assert ser_de(fory, EnumClass.E1) == EnumClass.E1
diff --git a/swift/Sources/ForyMacro/ForyObjectMacro.swift 
b/swift/Sources/ForyMacro/ForyObjectMacro.swift
index 63948c23a..03c57b3e5 100644
--- a/swift/Sources/ForyMacro/ForyObjectMacro.swift
+++ b/swift/Sources/ForyMacro/ForyObjectMacro.swift
@@ -196,6 +196,7 @@ private struct ParsedEnumCase {
     let name: String
     let payload: [ParsedEnumPayloadField]
     let caseID: Int?
+    let wireValue: UInt32?
 }
 
 private struct ParsedEnumDecl {
@@ -219,6 +220,7 @@ private struct ParsedForyObjectConfiguration {
 
 private func parseEnumDecl(_ enumDecl: EnumDeclSyntax) throws -> 
ParsedEnumDecl {
     var cases: [ParsedEnumCase] = []
+    let integerRawEnum = enumDeclUsesExplicitIntegerRawValues(enumDecl)
 
     for member in enumDecl.memberBlock.members {
         guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
@@ -275,7 +277,8 @@ private func parseEnumDecl(_ enumDecl: EnumDeclSyntax) 
throws -> ParsedEnumDecl
                 .init(
                     name: caseName,
                     payload: payloadFields,
-                    caseID: caseConfig?.id
+                    caseID: caseConfig?.id,
+                    wireValue: integerRawEnum ? 
parseEnumCaseWireValue(element) : nil
                 )
             )
         }
@@ -317,15 +320,19 @@ private func buildEnumDecls(_ parsedEnum: ParsedEnumDecl, 
accessPrefix: String)
 
 private func buildOrdinalEnumDecls(_ cases: [ParsedEnumCase], accessPrefix: 
String) -> [DeclSyntax] {
     let defaultCase = cases[0].name
+    let useExplicitWireValues = cases.allSatisfy { $0.wireValue != nil }
     let writeSwitchCases = cases.enumerated().map { index, enumCase in
-        """
+        let wireValue = enumCase.wireValue ?? UInt32(index)
+        return """
         case .\(enumCase.name):
-            context.buffer.writeVarUInt32(\(index))
+            context.buffer.writeVarUInt32(\(wireValue))
         """
     }.joined(separator: "\n        ")
     let readSwitchCases = cases.enumerated().map { index, enumCase in
-        "case \(index): return .\(enumCase.name)"
+        let wireValue = enumCase.wireValue ?? UInt32(index)
+        return "case \(wireValue): return .\(enumCase.name)"
     }.joined(separator: "\n        ")
+    let errorLabel = useExplicitWireValues ? "enum value" : "enum ordinal"
 
     let defaultDecl: DeclSyntax = DeclSyntax(
         stringLiteral: """
@@ -360,7 +367,7 @@ private func buildOrdinalEnumDecls(_ cases: 
[ParsedEnumCase], accessPrefix: Stri
             switch ordinal {
             \(readSwitchCases)
             default:
-                throw ForyError.invalidData("unknown enum ordinal \\(ordinal)")
+                throw ForyError.invalidData("unknown \(errorLabel) 
\\(ordinal)")
             }
         }
         """
@@ -369,6 +376,36 @@ private func buildOrdinalEnumDecls(_ cases: 
[ParsedEnumCase], accessPrefix: Stri
     return [defaultDecl, staticTypeIDDecl, writeWrapperDecl, writeDecl, 
readDecl]
 }
 
+private func enumDeclUsesExplicitIntegerRawValues(_ enumDecl: EnumDeclSyntax) 
-> Bool {
+    guard let inheritanceClause = enumDecl.inheritanceClause else {
+        return false
+    }
+    let inheritedTypes = inheritanceClause.inheritedTypes.map { 
$0.type.trimmedDescription }
+    return inheritedTypes.contains {
+        [
+            "Int",
+            "Int8",
+            "Int16",
+            "Int32",
+            "Int64",
+            "UInt",
+            "UInt8",
+            "UInt16",
+            "UInt32",
+            "UInt64",
+        ].contains($0)
+    }
+}
+
+private func parseEnumCaseWireValue(_ element: EnumCaseElementSyntax) -> 
UInt32? {
+    guard let rawValue = element.rawValue?.value.trimmedDescription,
+          let parsed = UInt32(rawValue)
+    else {
+        return nil
+    }
+    return parsed
+}
+
 private func buildTaggedUnionEnumDecls(_ cases: [ParsedEnumCase], 
accessPrefix: String) -> [DeclSyntax] {
     let defaultExpr = enumCaseDefaultExpr(cases[0])
     let writeSwitchCases = cases.enumerated().map { index, enumCase in
diff --git a/swift/Tests/ForyTests/ForySwiftTests.swift 
b/swift/Tests/ForyTests/ForySwiftTests.swift
index e7e347311..146138d86 100644
--- a/swift/Tests/ForyTests/ForySwiftTests.swift
+++ b/swift/Tests/ForyTests/ForySwiftTests.swift
@@ -80,6 +80,12 @@ struct FieldIdTarget: Equatable {
     var renamedLabel: String
 }
 
+@ForyObject
+enum SparseStatus: Int32, CaseIterable {
+    case unknown = 4096
+    case ok = 8192
+}
+
 @ForyObject
 struct EvolvingOverrideValue: Equatable {
     var f1: String = ""
@@ -724,6 +730,23 @@ func macroFieldEncodingOverridesForUnsignedTypes() throws {
     #expect(try buffer.readTaggedUInt64() == value.u64Tagged)
 }
 
+@Test
+func macroEnumUsesExplicitIntegerRawValue() throws {
+    let fory = Fory(config: .init(xlang: true, trackRef: false))
+    fory.register(SparseStatus.self, id: 302)
+
+    let data = try fory.serialize(SparseStatus.ok)
+    let buffer = ByteBuffer(data: data)
+    _ = try fory.readHead(buffer: buffer)
+    _ = try buffer.readInt8()
+    _ = try buffer.readVarUInt32()
+    _ = try buffer.readVarUInt32()
+    #expect(try buffer.readVarUInt32() == 8192)
+
+    let decoded: SparseStatus = try fory.deserialize(data)
+    #expect(decoded == .ok)
+}
+
 @Test
 func macroFieldEncodingOverridesCompatibleTypeMeta() throws {
     let fields = EncodedNumberFields.foryFieldsInfo(trackRef: false)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to