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 4527bc49c feat(xlang): refine xlang api and enum serialization (#3555)
4527bc49c is described below
commit 4527bc49c30fd12291105b2dd2ebb2fb2922f794
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Apr 12 18:21:29 2026 +0800
feat(xlang): refine xlang api and enum serialization (#3555)
## Why?
- Java enums needed a stable numeric tag option for cross-language and
schema-evolution scenarios that should not depend on declaration order.
- The xlang implementation guide and related docs/specs had drifted from
the current runtime architecture and API surface.
## What does this PR do?
- Remove `ForyBuilder.Xlang(bool)` from the C# public API, make `Config`
builder-created only, and make C# always emit and require the xlang
frame header.
- Add Java `@ForyEnumId` support so enums can use a single annotated
field, a zero-argument getter, or per-constant annotations for stable
numeric tags, with dense-array lookup for small IDs and sparse-map
lookup for larger IDs.
- Add Java serializer and xlang tests covering default ordinal behavior,
explicit enum IDs, validation failures, and cross-language enum
evolution with stable IDs.
- Refresh the C# guides and Java/xlang specs, including a rewrite of the
xlang implementation guide around the current `Fory` / `WriteContext` /
`ReadContext` / `RefWriter` / `RefReader` architecture.
## 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?
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
N/A
---
compiler/fory_compiler/generators/csharp.py | 2 +-
csharp/README.md | 1 -
csharp/src/Fory/Config.cs | 67 +--
csharp/src/Fory/Fory.cs | 8 +-
docs/guide/csharp/configuration.md | 14 +-
docs/guide/csharp/cross-language.md | 8 +-
docs/guide/csharp/troubleshooting.md | 10 +-
docs/guide/go/codegen.md | 22 +-
docs/guide/go/index.md | 17 +-
docs/guide/java/advanced-features.md | 2 +-
docs/guide/java/compression.md | 2 +-
docs/guide/java/configuration.md | 4 +-
docs/guide/java/cross-language.md | 2 +-
docs/guide/java/enum-configuration.md | 111 +++++
docs/guide/java/field-configuration.md | 2 +
docs/guide/java/index.md | 3 +
docs/guide/java/migration.md | 2 +-
docs/guide/java/row-format.md | 2 +-
docs/guide/java/schema-evolution.md | 2 +-
docs/guide/java/troubleshooting.md | 2 +-
docs/guide/java/virtual-threads.md | 2 +-
docs/specification/java_serialization_spec.md | 6 +-
docs/specification/xlang_implementation_guide.md | 476 +++++++++++----------
docs/specification/xlang_serialization_spec.md | 9 +-
.../idl_tests/csharp/IdlTests/RoundtripTests.cs | 1 -
.../org/apache/fory/annotation/ForyEnumId.java | 44 ++
.../org/apache/fory/serializer/EnumSerializer.java | 250 ++++++++++-
.../apache/fory/serializer/EnumSerializerTest.java | 184 ++++++++
.../test/java/org/apache/fory/xlang/EnumTest.java | 91 ++++
29 files changed, 1016 insertions(+), 330 deletions(-)
diff --git a/compiler/fory_compiler/generators/csharp.py
b/compiler/fory_compiler/generators/csharp.py
index 3fa27a52e..fc9fdcd81 100644
--- a/compiler/fory_compiler/generators/csharp.py
+++ b/compiler/fory_compiler/generators/csharp.py
@@ -872,7 +872,7 @@ class CSharpGenerator(BaseGenerator):
lines.append(f"{self.indent_str}private static ThreadSafeFory
CreateFory()")
lines.append(f"{self.indent_str}{{")
lines.append(
- f"{self.indent_str * 2}ThreadSafeFory fory =
Fory.Builder().Xlang(true).TrackRef(true).BuildThreadSafe();"
+ f"{self.indent_str * 2}ThreadSafeFory fory =
Fory.Builder().TrackRef(true).BuildThreadSafe();"
)
lines.append(f"{self.indent_str * 2}Register(fory);")
lines.append(f"{self.indent_str * 2}return fory;")
diff --git a/csharp/README.md b/csharp/README.md
index 456aeb86e..f00edfd67 100644
--- a/csharp/README.md
+++ b/csharp/README.md
@@ -216,7 +216,6 @@ Use consistent registration mappings across languages.
```csharp
Fory fory = Fory.Builder()
- .Xlang(true)
.Compatible(true)
.Build();
diff --git a/csharp/src/Fory/Config.cs b/csharp/src/Fory/Config.cs
index 4745053e0..25764103c 100644
--- a/csharp/src/Fory/Config.cs
+++ b/csharp/src/Fory/Config.cs
@@ -19,41 +19,53 @@ namespace Apache.Fory;
/// <summary>
/// Immutable runtime configuration used by <see cref="Fory"/> and <see
cref="ThreadSafeFory"/>.
+/// Instances are created by <see cref="ForyBuilder"/>.
/// </summary>
-/// <param name="Xlang">Whether cross-language protocol mode is
enabled.</param>
-/// <param name="TrackRef">Whether shared and circular reference tracking is
enabled.</param>
-/// <param name="Compatible">Whether schema-compatible mode is enabled.</param>
-/// <param name="CheckStructVersion">Whether generated struct schema hash
checks are enforced.</param>
-/// <param name="MaxDepth">Maximum allowed nesting depth for dynamic object
payload reads.</param>
-public sealed record Config(
- bool Xlang = true,
- bool TrackRef = false,
- bool Compatible = false,
- bool CheckStructVersion = false,
- int MaxDepth = 20);
+public sealed class Config
+{
+ internal Config(
+ bool trackRef,
+ bool compatible,
+ bool checkStructVersion,
+ int maxDepth)
+ {
+ TrackRef = trackRef;
+ Compatible = compatible;
+ CheckStructVersion = checkStructVersion;
+ MaxDepth = maxDepth;
+ }
+
+ /// <summary>
+ /// Gets whether shared and circular reference tracking is enabled.
+ /// </summary>
+ public bool TrackRef { get; }
+
+ /// <summary>
+ /// Gets whether schema-compatible mode is enabled.
+ /// </summary>
+ public bool Compatible { get; }
+
+ /// <summary>
+ /// Gets whether generated struct schema hash checks are enforced.
+ /// </summary>
+ public bool CheckStructVersion { get; }
+
+ /// <summary>
+ /// Gets the maximum allowed nesting depth for dynamic object payload
reads.
+ /// </summary>
+ public int MaxDepth { get; }
+}
/// <summary>
/// Fluent builder for creating <see cref="Fory"/> and <see
cref="ThreadSafeFory"/> runtimes.
/// </summary>
public sealed class ForyBuilder
{
- private bool _xlang = true;
private bool _trackRef;
private bool _compatible;
private bool _checkStructVersion;
private int _maxDepth = 20;
- /// <summary>
- /// Enables or disables cross-language protocol mode.
- /// </summary>
- /// <param name="enabled">Whether to enable cross-language mode. Defaults
to <c>true</c>.</param>
- /// <returns>The same builder instance.</returns>
- public ForyBuilder Xlang(bool enabled = true)
- {
- _xlang = enabled;
- return this;
- }
-
/// <summary>
/// Enables or disables reference tracking for shared and circular object
graphs.
/// </summary>
@@ -107,11 +119,10 @@ public sealed class ForyBuilder
private Config BuildConfig()
{
return new Config(
- Xlang: _xlang,
- TrackRef: _trackRef,
- Compatible: _compatible,
- CheckStructVersion: _checkStructVersion,
- MaxDepth: _maxDepth);
+ trackRef: _trackRef,
+ compatible: _compatible,
+ checkStructVersion: _checkStructVersion,
+ maxDepth: _maxDepth);
}
/// <summary>
diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs
index 555c94ab9..72c7fa38f 100644
--- a/csharp/src/Fory/Fory.cs
+++ b/csharp/src/Fory/Fory.cs
@@ -231,11 +231,7 @@ public sealed class Fory
/// <param name="isNone">Whether the payload value is null.</param>
internal void WriteHead(ByteWriter writer, bool isNone)
{
- byte bitmap = 0;
- if (Config.Xlang)
- {
- bitmap |= ForyHeaderFlag.IsXlang;
- }
+ byte bitmap = ForyHeaderFlag.IsXlang;
if (isNone)
{
@@ -255,7 +251,7 @@ public sealed class Fory
{
byte bitmap = reader.ReadUInt8();
bool peerIsXlang = (bitmap & ForyHeaderFlag.IsXlang) != 0;
- if (peerIsXlang != Config.Xlang)
+ if (!peerIsXlang)
{
throw new InvalidDataException("xlang bitmap mismatch");
}
diff --git a/docs/guide/csharp/configuration.md
b/docs/guide/csharp/configuration.md
index 0b75b2ee4..9d35ae0e5 100644
--- a/docs/guide/csharp/configuration.md
+++ b/docs/guide/csharp/configuration.md
@@ -20,6 +20,7 @@ license: |
---
This page covers `ForyBuilder` options and default configuration values for
Apache Fory™ C#.
+`Config` is an immutable runtime snapshot created by `ForyBuilder`.
## Build a Runtime
@@ -36,7 +37,6 @@ ThreadSafeFory threadSafe = Fory.Builder().BuildThreadSafe();
| Option | Default | Description
|
| -------------------- | ------- |
---------------------------------------------- |
-| `Xlang` | `true` | Cross-language protocol mode
|
| `TrackRef` | `false` | Reference tracking disabled
|
| `Compatible` | `false` | Schema-consistent mode (no evolution
metadata) |
| `CheckStructVersion` | `false` | Struct schema hash checks disabled
|
@@ -44,15 +44,8 @@ ThreadSafeFory threadSafe = Fory.Builder().BuildThreadSafe();
## Builder Options
-### `Xlang(bool enabled = true)`
-
-Controls cross-language mode.
-
-```csharp
-Fory fory = Fory.Builder()
- .Xlang(true)
- .Build();
-```
+C# always uses xlang-compatible framing, so `ForyBuilder` does not expose a
separate `Xlang(...)`
+toggle.
### `TrackRef(bool enabled = false)`
@@ -111,7 +104,6 @@ Fory fory = Fory.Builder()
```csharp
Fory fory = Fory.Builder()
- .Xlang(true)
.Compatible(true)
.TrackRef(true)
.Build();
diff --git a/docs/guide/csharp/cross-language.md
b/docs/guide/csharp/cross-language.md
index 5a2af307e..2b11ceb70 100644
--- a/docs/guide/csharp/cross-language.md
+++ b/docs/guide/csharp/cross-language.md
@@ -21,13 +21,14 @@ license: |
Apache Fory™ C# supports cross-language serialization with other Fory runtimes.
-## Enable Cross-Language Mode
+## Cross-Language Runtime
-C# defaults to `Xlang(true)`, but it is good practice to configure it
explicitly in interoperability code.
+C# always writes and reads the xlang frame header. There is no separate
`Xlang(...)` builder
+option, so interoperability code only needs to configure the remaining runtime
behavior such as
+compatibility mode and reference tracking.
```csharp
Fory fory = Fory.Builder()
- .Xlang(true)
.Compatible(true)
.Build();
```
@@ -43,7 +44,6 @@ public sealed class Person
}
Fory fory = Fory.Builder()
- .Xlang(true)
.Compatible(true)
.Build();
diff --git a/docs/guide/csharp/troubleshooting.md
b/docs/guide/csharp/troubleshooting.md
index f078627ee..6f2ec28e0 100644
--- a/docs/guide/csharp/troubleshooting.md
+++ b/docs/guide/csharp/troubleshooting.md
@@ -38,13 +38,15 @@ Ensure the same type-ID/name mapping exists on both write
and read sides.
## `InvalidDataException: xlang bitmap mismatch`
-**Cause**: Writer/reader disagree on `Xlang` mode.
+**Cause**: The payload is not an xlang Fory frame, or it came from a
peer/runtime mode that does
+not emit the xlang header C# requires.
-**Fix**: Use the same `Xlang(...)` value on both peers.
+**Fix**: Ensure the payload was produced by an xlang-compatible Fory runtime.
C# always expects the
+xlang header and does not expose a separate `Xlang(...)` builder option.
```csharp
-Fory writer = Fory.Builder().Xlang(true).Build();
-Fory reader = Fory.Builder().Xlang(true).Build();
+Fory writer = Fory.Builder().Compatible(true).Build();
+Fory reader = Fory.Builder().Compatible(true).Build();
```
## Schema Version Mismatch in Strict Mode
diff --git a/docs/guide/go/codegen.md b/docs/guide/go/codegen.md
index 19cb38458..d78bbfb83 100644
--- a/docs/guide/go/codegen.md
+++ b/docs/guide/go/codegen.md
@@ -20,18 +20,18 @@ license: |
---
:::warning Experimental Feature
-Code generation is an **experimental** feature in Fory Go. The API and
behavior may change in future releases. The reflection-based path remains the
stable, recommended approach for most use cases.
+Code generation is an **experimental** feature in Fory Go. The API and
behavior may change in future releases. The standard runtime path remains the
stable, recommended approach for most use cases.
:::
-Fory Go provides optional ahead-of-time (AOT) code generation for
performance-critical paths. This eliminates reflection overhead and provides
compile-time type safety.
+Fory Go provides optional ahead-of-time (AOT) code generation for
performance-critical paths. This generates dedicated serializers ahead of time
and adds compile-time shape checks.
## Why Code Generation?
-| Aspect | Reflection-Based | Code Generation |
+| Aspect | Standard Path | Code Generation |
| ----------- | ------------------ | ---------------------- |
| Setup | Zero configuration | Requires `go generate` |
-| Performance | Good | Better (no reflection) |
-| Type Safety | Runtime | Compile-time |
+| Performance | Excellent | Better on hot paths |
+| Type Safety | Runtime validation | Compile-time checks |
| Maintenance | Automatic | Requires regeneration |
**Use code generation when**:
@@ -40,7 +40,7 @@ Fory Go provides optional ahead-of-time (AOT) code generation
for performance-cr
- Compile-time type safety is important
- Hot paths are performance-critical
-**Use reflection when**:
+**Use the standard path when**:
- Simple setup is preferred
- Types change frequently
@@ -307,7 +307,7 @@ type HotPathStruct struct {
}
type ColdPathStruct struct {
- // Not annotated, uses reflection
+ // Not annotated, uses the standard runtime serializer
}
```
@@ -326,12 +326,12 @@ type ColdPathStruct struct {
- Private (unexported) fields
- Custom serializers
-### Reflection Fallback
+### Standard Path Fallback
-If codegen fails, Fory falls back to reflection:
+If generated serializers are unavailable, Fory falls back to the standard
serializer path:
```go
-// If User_ForyGenSerializer not found, uses reflection
+// If User_ForyGenSerializer is not linked in, Fory uses the standard path
f.Serialize(&User{})
```
@@ -405,7 +405,7 @@ type User struct {
### Is codegen required?
-No. Reflection-based serialization works without code generation.
+No. The standard serializer path works without code generation.
### Does generated code work across Go versions?
diff --git a/docs/guide/go/index.md b/docs/guide/go/index.md
index 90dba1b61..68ef08c54 100644
--- a/docs/guide/go/index.md
+++ b/docs/guide/go/index.md
@@ -27,7 +27,7 @@ Apache Fory Go is a high-performance, cross-language
serialization library for G
- **Cross-Language**: Seamless data exchange with Java, Python, C++, Rust, and
JavaScript
- **Automatic Serialization**: No IDL definitions or schema compilation
required
- **Reference Tracking**: Built-in support for circular references and shared
objects
-- **Type Safety**: Strong typing with compile-time verification (optional
codegen)
+- **Type Safety**: Strong typing with schema-aware serializers
- **Schema Evolution**: Compatible mode for forward/backward compatibility
- **Thread-Safe Option**: Pool-based thread-safe wrapper for concurrent use
@@ -84,23 +84,17 @@ func main() {
}
```
-## Architecture
+## Default Serialization Path
-Fory Go provides two serialization paths:
-
-### Reflection-Based (Default)
-
-The default path uses Go's reflection to inspect types at runtime. This works
out-of-the-box with any struct. Although this mode uses reflection, it is
highly optimized with type caching, inlined hot paths, delivering excellent
performance for most use cases:
+Fory Go works out-of-the-box with ordinary Go structs. The standard runtime
path caches type
+metadata and keeps hot serialization paths optimized, so most applications can
use it directly
+without any extra build step:
```go
f := fory.New()
data, _ := f.Serialize(myStruct)
```
-### Code Generation (Experimental)
-
-For performance-critical paths, Fory provides optional ahead-of-time code
generation that eliminates reflection overhead. See the [Code
Generation](codegen.md) guide for details.
-
## Configuration
Fory Go uses a functional options pattern for configuration:
@@ -153,7 +147,6 @@ See [Cross-Language Serialization](cross-language.md) for
type mapping and compa
| [Struct Tags](struct-tags.md) | Field-level configuration
|
| [Schema Evolution](schema-evolution.md) | Forward/backward
compatibility |
| [Cross-Language](cross-language.md) | Multi-language serialization
|
-| [Code Generation](codegen.md) | Experimental AOT code
generation |
| [Thread Safety](thread-safety.md) | Concurrent usage patterns
|
| [Troubleshooting](troubleshooting.md) | Common issues and solutions
|
diff --git a/docs/guide/java/advanced-features.md
b/docs/guide/java/advanced-features.md
index b7a535fe4..c45d700d1 100644
--- a/docs/guide/java/advanced-features.md
+++ b/docs/guide/java/advanced-features.md
@@ -1,6 +1,6 @@
---
title: Advanced Features
-sidebar_position: 7
+sidebar_position: 10
id: advanced_features
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/java/compression.md b/docs/guide/java/compression.md
index efe261aad..88cfb70a6 100644
--- a/docs/guide/java/compression.md
+++ b/docs/guide/java/compression.md
@@ -1,6 +1,6 @@
---
title: Compression
-sidebar_position: 6
+sidebar_position: 7
id: compression
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/java/configuration.md b/docs/guide/java/configuration.md
index 9071d423b..dc1025bcd 100644
--- a/docs/guide/java/configuration.md
+++ b/docs/guide/java/configuration.md
@@ -47,7 +47,7 @@ This page documents all configuration options available
through `ForyBuilder`.
| `asyncCompilationEnabled` | If enabled, serialization uses
interpreter mode first and switches to JIT serialization after async serializer
JIT for a class is finished.
[...]
| `scalaOptimizationEnabled` | Enables or disables Scala-specific
serialization optimization.
[...]
| `copyRef` | When disabled, the copy performance
will be better. But fory deep copy will ignore circular and shared reference.
Same reference of an object graph will be copied into different objects in one
`Fory#copy`.
[...]
-| `serializeEnumByName` | When Enabled, fory serialize enum by
name instead of ordinal.
[...]
+| `serializeEnumByName` | When enabled, Fory serializes enum
names instead of numeric enum tags. Without this option, Fory writes
declaration ordinals by default, or explicit stable ids when the enum is
configured with `@ForyEnumId`.
[...]
## Example Configuration
@@ -72,6 +72,8 @@ Fory fory = Fory.builder()
## Related Topics
+- [Field Configuration](field-configuration.md) - `@ForyField`, `@Ignore`, and
integer encoding annotations
+- [Enum Configuration](enum-configuration.md) - `serializeEnumByName` and
`@ForyEnumId`
- [Schema Evolution](schema-evolution.md) - Compatible mode and meta sharing
- [Compression](compression.md) - Int, long, and array compression details
- [Type Registration](type-registration.md) - Class registration options
diff --git a/docs/guide/java/cross-language.md
b/docs/guide/java/cross-language.md
index c9421245f..31776ab7c 100644
--- a/docs/guide/java/cross-language.md
+++ b/docs/guide/java/cross-language.md
@@ -1,6 +1,6 @@
---
title: Cross-Language Serialization
-sidebar_position: 8
+sidebar_position: 11
id: cross_language
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/java/enum-configuration.md
b/docs/guide/java/enum-configuration.md
new file mode 100644
index 000000000..c38507316
--- /dev/null
+++ b/docs/guide/java/enum-configuration.md
@@ -0,0 +1,111 @@
+---
+title: Enum Configuration
+sidebar_position: 6
+id: enum_configuration
+license: |
+ 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.
+---
+
+This page explains how Java enum serialization is configured in Apache Fory.
+
+## Default Enum Behavior
+
+Java enums can be serialized in two modes:
+
+1. **By numeric tag**: the default behavior
+2. **By enum name**: enabled with `serializeEnumByName(true)`
+
+Numeric tags are always used in xlang mode. In native Java mode,
`serializeEnumByName(true)`
+switches enum serialization to names instead of numeric tags.
+
+## Serialize Enums by Name
+
+Use `serializeEnumByName(true)` when native Java peers should match enum
constants by name instead
+of numeric tag.
+
+```java
+Fory fory = Fory.builder()
+ .withLanguage(Language.JAVA)
+ .serializeEnumByName(true)
+ .build();
+```
+
+This mode is useful when declaration order is unstable but enum names remain
fixed. It only affects
+native Java mode. Xlang still uses numeric tags.
+
+## Stable Numeric Enum IDs
+
+Without `serializeEnumByName(true)`, Java enums are serialized by numeric tag.
The default tag is
+the declaration ordinal. When an enum needs stable ids that do not depend on
declaration order,
+annotate exactly one id source with `@ForyEnumId`, or annotate every enum
constant with explicit
+tag values.
+
+```java
+import org.apache.fory.annotation.ForyEnumId;
+
+enum Status {
+ Unknown(10),
+ Running(20),
+ Finished(30);
+
+ private final int id;
+
+ Status(int id) {
+ this.id = id;
+ }
+
+ @ForyEnumId
+ public int getId() {
+ return id;
+ }
+}
+```
+
+Java also supports annotating one enum instance field with `@ForyEnumId`, or
annotating every enum
+constant directly such as `@ForyEnumId(10) Unknown`.
+
+### `@ForyEnumId` Styles
+
+`@ForyEnumId` supports exactly three configuration styles:
+
+1. Annotate one enum instance field and store the numeric id there.
+2. Annotate one zero-argument public instance method such as `getId()`.
+3. Annotate every enum constant directly with an explicit value such as
`@ForyEnumId(10) Unknown`.
+
+### Validation Rules
+
+1. Use exactly one of those three styles for a given enum.
+2. Field and method annotations must leave `value()` at its default `-1`.
+3. Enum-constant annotations must appear on every constant once any constant
uses `@ForyEnumId`.
+4. All ids must be non-negative, unique, and fit in Java `int`.
+
+### Lookup Behavior
+
+1. Without `@ForyEnumId`, Fory writes the declaration ordinal.
+2. With `@ForyEnumId`, Fory writes the configured stable numeric tag instead.
+3. Small dense tags use an array lookup internally; sparse larger tags fall
back to a map.
+
+## Choosing Between Name and Numeric Modes
+
+- Use **enum names** when the enum is Java-only and constant names are the
intended compatibility key.
+- Use **numeric tags** when cross-language payloads or stable explicit ids
matter.
+- Use **`@ForyEnumId`** when declaration order may change but the numeric wire
ids must stay stable.
+
+## Related Topics
+
+- [Configuration](configuration.md) - `serializeEnumByName` and other runtime
options
+- [Field Configuration](field-configuration.md) - `@ForyField`, `@Ignore`, and
integer encoding annotations
+- [Cross-Language](cross-language.md) - Xlang enum interoperability
diff --git a/docs/guide/java/field-configuration.md
b/docs/guide/java/field-configuration.md
index 20a82c197..6051090f6 100644
--- a/docs/guide/java/field-configuration.md
+++ b/docs/guide/java/field-configuration.md
@@ -601,5 +601,7 @@ public class User {
## Related Topics
- [Basic Serialization](basic-serialization.md) - Getting started with Fory
serialization
+- [Configuration](configuration.md) - Runtime builder options
+- [Enum Configuration](enum-configuration.md) - `@ForyEnumId` and enum
name/tag behavior
- [Schema Evolution](schema-evolution.md) - Compatible mode and schema
evolution
- [Cross-Language](cross-language.md) - Interoperability with Python, Rust,
C++, Go
diff --git a/docs/guide/java/index.md b/docs/guide/java/index.md
index 2ce65869a..e4b7d05ac 100644
--- a/docs/guide/java/index.md
+++ b/docs/guide/java/index.md
@@ -200,7 +200,10 @@ ThreadSafeFory threadLocalFory = Fory.builder()
## Next Steps
- [Configuration](configuration.md) - Learn about ForyBuilder options
+- [Field Configuration](field-configuration.md) - `@ForyField`, `@Ignore`, and
integer encoding annotations
+- [Enum Configuration](enum-configuration.md) - `serializeEnumByName` and
`@ForyEnumId`
- [Basic Serialization](basic-serialization.md) - Detailed serialization
patterns
+- [Compression](compression.md) - Integer, long, and array compression options
- [Virtual Threads](virtual-threads.md) - Virtual-thread usage and pool sizing
guidance
- [Type Registration](type-registration.md) - Class registration and security
- [Custom Serializers](custom-serializers.md) - Implement custom serializers
diff --git a/docs/guide/java/migration.md b/docs/guide/java/migration.md
index 20d41bede..9c77b3564 100644
--- a/docs/guide/java/migration.md
+++ b/docs/guide/java/migration.md
@@ -1,6 +1,6 @@
---
title: Migration Guide
-sidebar_position: 10
+sidebar_position: 13
id: migration
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/java/row-format.md b/docs/guide/java/row-format.md
index eb58646b3..14bb55ed4 100644
--- a/docs/guide/java/row-format.md
+++ b/docs/guide/java/row-format.md
@@ -1,6 +1,6 @@
---
title: Row Format
-sidebar_position: 9
+sidebar_position: 12
id: row_format
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/java/schema-evolution.md
b/docs/guide/java/schema-evolution.md
index 0ea207fa2..5b754171a 100644
--- a/docs/guide/java/schema-evolution.md
+++ b/docs/guide/java/schema-evolution.md
@@ -1,6 +1,6 @@
---
title: Schema Evolution
-sidebar_position: 5
+sidebar_position: 9
id: schema_evolution
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/java/troubleshooting.md
b/docs/guide/java/troubleshooting.md
index ec0e778be..b693d51bc 100644
--- a/docs/guide/java/troubleshooting.md
+++ b/docs/guide/java/troubleshooting.md
@@ -1,6 +1,6 @@
---
title: Troubleshooting
-sidebar_position: 11
+sidebar_position: 14
id: troubleshooting
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/guide/java/virtual-threads.md
b/docs/guide/java/virtual-threads.md
index 5db05a951..cc57166b3 100644
--- a/docs/guide/java/virtual-threads.md
+++ b/docs/guide/java/virtual-threads.md
@@ -1,6 +1,6 @@
---
title: Virtual Threads
-sidebar_position: 11
+sidebar_position: 8
id: java_virtual_threads
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/docs/specification/java_serialization_spec.md
b/docs/specification/java_serialization_spec.md
index a6957370b..2bac7bc0d 100644
--- a/docs/specification/java_serialization_spec.md
+++ b/docs/specification/java_serialization_spec.md
@@ -359,7 +359,11 @@ UTF16 is encoded as little endian 2-byte code units.
### Enum
- If `serializeEnumByName` is enabled: write enum name as a meta string.
-- Otherwise: write enum ordinal as varuint32 small7.
+- Otherwise: write an enum tag as varuint32 small7.
+ - By default the tag is the declaration ordinal.
+ - If the enum configures `@ForyEnumId`, write the configured stable id
instead. Java supports
+ annotating exactly one id field, exactly one zero-argument id getter, or
every enum constant
+ with explicit tag values.
### Binary (byte[])
diff --git a/docs/specification/xlang_implementation_guide.md
b/docs/specification/xlang_implementation_guide.md
index 77b808cdd..a0f5afaf3 100644
--- a/docs/specification/xlang_implementation_guide.md
+++ b/docs/specification/xlang_implementation_guide.md
@@ -19,239 +19,273 @@ license: |
limitations under the License.
---
-## Implementation guidelines
-
-### How to reduce memory read/write code
-
-- Try to merge multiple bytes into an int/long write before writing to reduce
memory IO and bound check cost.
-- Read multiple bytes as an int/long, then split into multiple bytes to reduce
memory IO and bound check cost.
-- Try to use one varint/long to write flags and length together to save one
byte cost and reduce memory io.
-- Condition branches are less expensive compared to memory IO cost unless
there are too many branches.
-
-### Fast deserialization for static languages without runtime codegen support
-
-For type evolution, the serializer will encode the type meta into the
serialized data. The deserializer will compare
-this meta with class meta in the current process, and use the diff to
determine how to deserialize the data.
-
-For java/javascript/python, we can use the diff to generate serializer code at
runtime and load it as class/function for
-deserialization. In this way, the type evolution will be as fast as type
consist mode.
-
-For C++/Rust, we can't generate the serializer code at runtime. So we need to
generate the code at compile-time using
-meta programming. But at that time, we don't know the type schema in other
processes, so we can't generate the
-serializer code for such inconsistent types. We may need to generate the code
which has a loop and compare field name
-one by one to decide whether to deserialize and assign the field or skip the
field value.
-
-One fast way is that we can optimize the string comparison into `jump`
instructions:
-
-- Assume the current type has `n` fields, and the peer type has `n1` fields.
-- Generate an auto growing `field id` from `0` for every sorted field in the
current type at the compile time.
-- Compare the received type meta with current type, generate same id if the
field name is same, otherwise generate an
- auto growing id starting from `n`, cache this meta at runtime.
-- Iterate the fields of received type meta, use a `switch` to compare the
`field id` to deserialize data
- and `assign/skip` field value. **Continuous** field id will be optimized
into `jump` in `switch` block, so it will
- very fast.
-
-Here is an example, suppose process A has a class `Foo` with version 1 defined
as `Foo1`, process B has a class `Foo`
-with version 2 defined as `Foo2`:
-
-```c++
-// class Foo with version 1
-class Foo1 {
- int32_t v1; // id 0
- std::string v2; // id 1
-};
-// class Foo with version 2
-class Foo2 {
- // id 0, but will have id 2 in process A
- bool v0;
- // id 1, but will have id 0 in process A
- int32_t v1;
- // id 2, but will have id 3 in process A
- int64_t long_value;
- // id 3, but will have id 1 in process A
- std::string v2;
- // id 4, but will have id 4 in process A
- std::vector<std::string> list;
-};
-```
-
-When process A received serialized `Foo2` from process B, here is how it
deserialize the data:
-
-```c++
-Foo1 foo1 = ...;
-const std::vector<fory::FieldInfo> &field_infos = type_meta.field_infos;
-for (const auto &field_info : field_infos) {
- switch (field_info.field_id) {
- case 0:
- foo1.v1 = buffer.read_varint32();
- break;
- case 1:
- foo1.v2 = fory.read_string();
- break;
- default:
- fory.skip_data(field_info);
- }
-}
-```
-
-## Implementation Checklist for New Languages
-
-This section provides a step-by-step guide for implementing Fory xlang
serialization in a new language.
-
-### Phase 1: Core Infrastructure
-
-1. **Buffer Implementation**
- - [ ] Create a byte buffer with read/write cursor tracking
- - [ ] Implement little-endian byte order for all multi-byte writes
- - [ ] Implement `write_int8`, `write_int16`, `write_int32`, `write_int64`
- - [ ] Implement `write_float32`, `write_float64`
- - [ ] Implement `read_*` counterparts for all write methods
- - [ ] Implement buffer growth strategy (e.g., doubling)
-
-2. **Varint Encoding**
- - [ ] Implement `write_varuint32` / `read_varuint32`
- - [ ] Implement `write_varint32` / `read_varint32` (with ZigZag)
- - [ ] Implement `write_varuint64` / `read_varuint64`
- - [ ] Implement `write_varint64` / `read_varint64` (with ZigZag)
- - [ ] Implement `write_varuint36_small` / `read_varuint36_small` (for
strings)
- - [ ] Optionally implement Hybrid encoding (TAGGED_INT64/TAGGED_UINT64) for
int64
-
-3. **Header Handling**
- - [ ] Write/read bitmap flags (null, xlang, oob)
-
-### Phase 2: Basic Type Serializers
-
-4. **Primitive Types**
- - [ ] bool (1 byte: 0 or 1)
- - [ ] int8, int16, int32, int64 (little endian)
- - [ ] float32, float64 (IEEE 754, little endian)
-
-5. **String Serialization**
- - [ ] Implement string header: `(byte_length << 2) | encoding`
- - [ ] Support UTF-8 encoding (required for xlang)
- - [ ] Optionally support LATIN1 and UTF-16
-
-6. **Temporal Types**
- - [ ] Duration (seconds + nanoseconds)
- - [ ] Timestamp (seconds + nanoseconds since epoch)
- - [ ] Date (days since epoch)
-
-7. **Reference Tracking**
- - [ ] Implement write-side object tracking (object → ref_id map)
- - [ ] Implement read-side object tracking (ref_id → object list)
- - [ ] Handle all four reference flags: NULL(-3), REF(-2), NOT_NULL(-1),
REF_VALUE(0)
- - [ ] Support disabling reference tracking per-type or globally
-
-### Phase 3: Collection Types
-
-8. **List/Array Serialization**
- - [ ] Write length as varuint32
- - [ ] Write elements header byte
- - [ ] Handle homogeneous vs heterogeneous elements
- - [ ] Handle null elements
-
-9. **Map Serialization**
- - [ ] Write total size as varuint32
- - [ ] Implement chunk-based format (max 255 pairs per chunk)
- - [ ] Write KV header byte per chunk
- - [ ] Handle key and value type variations
-
-10. **Set Serialization**
- - [ ] Same format as List (reuse implementation)
-
-### Phase 4: Meta String Encoding
-
-Meta strings are required for enum and struct serialization (encoding field
names, type names, namespaces).
-
-11. **Meta String Compression**
- - [ ] Implement LOWER_SPECIAL encoding (5 bits/char)
- - [ ] Implement LOWER_UPPER_DIGIT_SPECIAL encoding (6 bits/char)
- - [ ] Implement FIRST_TO_LOWER_SPECIAL encoding
- - [ ] Implement ALL_TO_LOWER_SPECIAL encoding
- - [ ] Implement encoding selection algorithm
- - [ ] Implement meta string deduplication
-
-### Phase 5: Enum Serialization
-
-12. **Enum Serialization**
- - [ ] Write ordinal as varuint32
- - [ ] Support named enum (namespace + type name)
-
-### Phase 6: Struct Serialization
-
-13. **Type Registration**
- - [ ] Support registration by numeric ID
- - [ ] Support registration by namespace + type name
- - [ ] Maintain type → serializer mapping
- - [ ] Generate type IDs: write internal type ID, then `user_type_id` as
varuint32
-
-14. **Field Ordering**
- - [ ] Implement the spec-defined grouping and ordering
(primitive/boxed/built-in, collections/maps, other)
- - [ ] Use a stable comparator within each group (type ID and name)
- - [ ] Use tag ID or snake_case field name as field identifier for
fingerprints
+This document describes the current Java xlang runtime architecture. The wire
format is defined by
+[Xlang Serialization Spec](xlang_serialization_spec.md); this guide explains
the service
+boundaries and control flow that the reference implementation uses today. New
runtimes do not need
+the same class names, but they should preserve the same ownership model: root
operations stay on
+the runtime facade, while payload work stays on explicit read and write
contexts.
-15. **Schema Consistent Mode**
- - [ ] If class-version check is enabled, compute schema hash from field
identifiers
- - [ ] Write 4-byte schema hash before fields
- - [ ] Serialize fields in Fory order
+## Runtime ownership model
-16. **Compatible/Meta Share Mode**
- - [ ] Implement shared TypeDef stream (inline new TypeDefs, index
references)
- - [ ] Map fields by name or tag ID, skip unknown fields
- - [ ] Apply nullable/ref flags from TypeDef metadata
-
-### Phase 7: Other types
-
-17. **Binary/Array Types**
-
-- [ ] Primitive arrays (direct buffer copy)
-- [ ] Multi-dimensional arrays as nested lists (no tensor encoding)
+### `Fory` is the root-operation facade
-### Testing Strategy
+`Fory` owns the immutable `Config`, the active `TypeResolver`, the
`JITContext`, and one reusable
+`WriteContext`, `ReadContext`, and `CopyContext` for that runtime instance.
Top-level
+`serialize(...)`, `deserialize(...)`, and `copy(...)` entry points live here.
-18. **Cross-Language Compatibility Tests**
- - [ ] Serialize in new language, deserialize in Java/Python
- - [ ] Serialize in Java/Python, deserialize in new language
- - [ ] Test all primitive types
- - [ ] Test strings with various encodings
- - [ ] Test collections (empty, single, multiple elements)
- - [ ] Test maps with various key/value types
- - [ ] Test nested structs
- - [ ] Test circular references (if supported)
+Before the first root operation, `Fory` freezes registration by calling
+`TypeResolver.finishRegistration()`. After that point, serializers and type
IDs are treated as
+stable for the lifetime of the runtime.
-## Language-Specific Implementation Notes
+`Fory` is deliberately not the place where nested serializers do their work.
During an active root
+operation, nested calls back into `Fory.serializeXXX` or `Fory.deserializeXXX`
are rejected. Inside
+serializers, nested payload handling must go through `WriteContext` and
`ReadContext`.
-### Java
+### `WriteContext` and `ReadContext` hold all operation-local state
-- Uses runtime code generation (JIT) for maximum performance
-- Supports all reference tracking modes
-- Uses internal String coder for encoding selection
-- Thread-safe via `ThreadSafeFory` wrapper
+`WriteContext` and `ReadContext` are prepared by `Fory` for one root operation
and reset in a
+`finally` block before reuse. They hold:
-### Python
+- the current `MemoryBuffer`
+- the shared `Generics` stack
+- the active `TypeResolver`
+- the active `RefWriter` or `RefReader`
+- meta-string and meta-share state
+- operation-local scratch state keyed by object identity
+- the logical object-graph depth
+- out-of-band buffer state on the read side
-- Two modes: Pure Python (debugging) and Cython (performance)
-- Uses `id(obj)` for reference tracking
-- Latin1/UTF-16/UTF-8 encoding for all strings in xlang mode
-- `dataclass` support via code generation
+Generated and hand-written serializers should treat these contexts as the only
source of
+operation-local services. Serializers must not keep ambient runtime state in
thread locals or in
+serializer instance fields.
-### C++
+### Reference tracking is a pluggable service
-- Compile-time reflection via macros (`FORY_STRUCT`)
-- Template meta programming for type dispatch and serializer selection
-- Uses `std::shared_ptr` for reference tracking
-- Compile-time field ordering
-- No runtime code generation
+Reference handling is split behind two small interfaces:
-### Rust
+- `RefWriter` writes null, reference, and new-value markers and remembers
previously written
+ objects by identity.
+- `RefReader` decodes those markers, reserves read reference IDs, and resolves
previously
+ materialized objects.
-- Derive macros for automatic serialization (`#[derive(ForyObject)]`)
-- Uses `Rc<T>` / `Arc<T>` for reference tracking
-- Thread-local context caching for performance
-- Compile-time field ordering
+When reference tracking is enabled, Java uses `MapRefWriter` and
`MapRefReader`. When it is
+disabled, Java swaps in `NoRefWriter` and `NoRefReader`, which keep the same
call shape while
+avoiding map and array maintenance.
-### Go
+### Type resolution is a separate service
-- Reflection-based and codegen-based modes
-- Struct tags for field annotations
-- Interface types for polymorphism
+`TypeResolver` owns serializer lookup, type registration, type metadata
encoding, and the caches
+used while reading type info from the stream.
+
+In xlang mode, Java uses `XtypeResolver`. In native Java mode, it uses
`ClassResolver`. The rest of
+the runtime talks to the abstract `TypeResolver` contract.
+
+## Root frame responsibilities
+
+Every root payload starts with a one-byte bitmap written and read by `Fory`
itself, not by
+serializers:
+
+| Bit | Meaning |
+| --- | ------------------------------- |
+| `0` | null root payload |
+| `1` | xlang payload |
+| `2` | out-of-band buffers are enabled |
+
+Per-object reference markers are separate from that root bitmap. Java uses
these signed marker
+bytes throughout the object graph:
+
+| Value | Meaning |
+| ----- | --------------------- |
+| `-3` | `NULL_FLAG` |
+| `-2` | `REF_FLAG` |
+| `-1` | `NOT_NULL_VALUE_FLAG` |
+| `0` | `REF_VALUE_FLAG` |
+
+Keep those two layers separate in every runtime:
+
+- the root bitmap describes the whole payload
+- ref flags describe one nested value at a time
+
+## Serialization flow
+
+### Root write path
+
+The current Java xlang write path is:
+
+1. `Fory.serialize(...)` calls `ensureRegistrationFinished()`.
+2. `Fory` binds the target buffer and optional `BufferCallback` with
`writeContext.prepare(...)`.
+3. `Fory` writes the root bitmap.
+4. If the root value is non-null, `Fory` locks the `JITContext`, verifies that
this is not a
+ nested root call, and delegates the root object to
`writeContext.writeRef(obj)`.
+5. `writeContext.reset()` runs in `finally`, regardless of success or failure.
+
+`WriteContext.writeRef(...)` is the main object-graph entry point:
+
+1. `RefWriter.writeRefOrNull(...)` emits the null, ref, or new-value marker.
+2. If the object is new, `WriteContext` resolves `TypeInfo` from the active
`TypeResolver`.
+3. For most types, `TypeResolver.writeTypeInfo(...)` writes the xlang type
header.
+4. `WriteContext.writeData(...)` writes the payload. Primitive and string-like
hot paths write
+ directly to `MemoryBuffer`; other types delegate to the resolved serializer.
+
+The xlang `UnknownStruct` path is the main special case: it owns its own
stream representation and
+does not follow the normal "write type info, then payload" sequence.
+
+### Payload serializers write through `WriteContext`
+
+Serializers are responsible only for the payload of their type. They do not
write the root bitmap,
+own registration, or decide how class metadata is encoded.
+
+Important current Java rules:
+
+- Serializer instances are runtime-local by default. Only serializers that
implement `Shareable`
+ may be reused across equivalent runtimes.
+- Use `WriteContext` helpers such as `writeRef(...)`, `writeNonRef(...)`,
`writeStringRef(...)`,
+ and `writeBufferObject(...)` when nested values need ref handling or type
metadata.
+- If several primitive writes happen in a row, fetch `MemoryBuffer` once from
+ `WriteContext.getBuffer()` and write directly for better inlining and fewer
helper calls.
+- `WriteContext` maintains `depth` around nested serializer calls. That depth
is also used to block
+ illegal nested root operations.
+
+## Deserialization flow
+
+### Root read path
+
+The current Java xlang read path mirrors the write path:
+
+1. `Fory.deserialize(...)` calls `ensureRegistrationFinished()`.
+2. `Fory` reads the root bitmap.
+3. If the null bit is set, deserialization returns `null` immediately.
+4. `Fory` verifies that the payload xlang bit matches the runtime mode.
+5. `Fory` validates whether out-of-band buffers must or must not be supplied.
+6. `Fory` binds the buffer and optional out-of-band buffer iterator with
+ `readContext.prepare(...)`.
+7. `Fory` locks the `JITContext`, verifies that this is not a nested root
call, and delegates to
+ `readContext.readRef()` or the typed `deserializeByType(...)` path.
+8. `readContext.reset()` runs in `finally`.
+
+### `ReadContext` owns reference reservation and payload materialization
+
+`ReadContext.readRef()` performs the normal xlang read sequence:
+
+1. `RefReader.tryPreserveRefId(...)` consumes the next ref marker.
+2. If the marker is `REF_FLAG`, the previously materialized object is returned
immediately.
+3. If the marker is `NULL_FLAG`, `null` is returned.
+4. If the marker indicates a new value, the reader reserves a dense read
reference ID before the
+ payload is materialized.
+5. `TypeResolver.readTypeInfo(...)` decodes the type header.
+6. `ReadContext.readNonRef(typeInfo)` reads the payload.
+7. `RefReader.setReadRef(...)` binds the reserved ID to the completed object.
+
+Primitive and string-like hot paths read directly from `MemoryBuffer`; complex
payloads delegate to
+the resolved serializer. This reservation-before-read pattern is what lets
Java support cycles and
+back-references to partially built objects inside containers and structs.
+
+### Serializers must bind newly created objects early when needed
+
+Many serializers allocate the target object before all child values have been
read. In that case,
+the serializer must register the partially built object with
`readContext.reference(obj)` or
+`readContext.setReadRef(...)` before reading nested children that may point
back to it.
+
+That rule is essential for arrays, collections, maps, object serializers,
meta-share serializers,
+replace/resolve serializers, and any other serializer that can participate in
cycles.
+
+### Read-side depth and security
+
+`ReadContext` tracks logical object depth. `increaseDepth()` enforces
`Config.maxDepth()` and
+throws if the stream looks malicious or unexpectedly deep. New runtimes should
keep the same
+explicit depth accounting instead of relying on the native call stack alone.
+
+## Type metadata and xlang type resolution
+
+### `TypeResolver` writes and reads all xlang type headers
+
+`TypeResolver.writeTypeInfo(...)` always writes the 8-bit type ID first, then
emits any extra type
+metadata required by that kind:
+
+- registered user enum, struct, ext, and typed union types write the user type
ID
+- named types write namespace and type-name meta strings when meta share is
disabled
+- compatible struct modes write shared `TypeDef` metadata
+- built-in types write only the internal type ID
+
+`TypeResolver.readTypeInfo(...)` is the inverse operation. It decodes the type
ID, consumes any
+attached metadata, returns the matching `TypeInfo`, and ensures that a
serializer exists before the
+payload is read.
+
+### `XtypeResolver` is the xlang-specific implementation
+
+`XtypeResolver` extends `TypeResolver` with xlang-specific registration and
lookup rules:
+
+- it assigns xlang user type IDs
+- it registers built-in xlang serializers
+- it resolves named types from namespace and type-name bytes
+- it handles `UnknownStruct` and other unknown-class cases
+- it builds or loads meta-shared serializers when compatible struct metadata
is used
+
+The important design point is that serializers do not resolve class metadata
themselves. They ask
+the current context for nested reads and writes, and the context delegates
type work to
+`TypeResolver`.
+
+For typed Java entry points, `Fory.deserialize(..., Class<T>)` also pushes the
expected generic
+type onto the shared `Generics` stack before reading and pops it afterward.
+
+## Meta strings and meta-share state
+
+Two pieces of explicit runtime state back xlang type metadata:
+
+- `MetaStringWriter` and `MetaStringReader` deduplicate and decode namespace
and type-name strings
+- `MetaWriteContext` and `MetaReadContext` track shared `TypeDef`
announcements for meta-share mode
+
+When scoped meta share is enabled, each `WriteContext` and `ReadContext` owns
its own meta-share
+state for one root operation and clears it during `reset()`.
+
+When scoped meta share is disabled, callers may install externally owned
`MetaWriteContext` and
+`MetaReadContext` instances through `setMetaWriteContext(...)` and
`setMetaReadContext(...)` so the
+same meta-share session can span multiple root operations.
+
+This state is explicit on the contexts. It is not hidden in globals or
thread-local caches.
+
+## Enums in xlang mode
+
+In Java xlang mode, enums are serialized by numeric tag, not by name.
+
+- By default, the tag is the declaration ordinal.
+- If the enum is configured with `@ForyEnumId`, Java writes that explicit
stable tag instead.
+- `serializeEnumByName(true)` only changes native Java mode; xlang still uses
numeric tags.
+
+`EnumSerializer` precomputes two structures from the chosen tags:
+
+- `tagByOrdinal` for the write path
+- either a dense `Enum[]` lookup table or a sparse `Map<Integer, Enum>` for
the read path
+
+Small explicit ID spaces use the array fast path. Large sparse ID spaces use
the map fast path.
+
+## Out-of-band buffer objects
+
+`WriteContext.writeBufferObject(...)` and `ReadContext.readBufferObject()`
implement the current
+buffer-object contract:
+
+- one boolean says whether the bytes are in-band or out-of-band
+- in-band payloads encode the byte length and then the raw bytes
+- out-of-band payloads rely on the caller-supplied `BufferCallback` and
out-of-band buffer iterator
+
+The root bitmap advertises whether out-of-band buffers are in play for the
whole payload. Runtime
+validation happens in `Fory.deserialize(...)` before nested serializers start
reading.
+
+## Serializer design rules for new runtimes
+
+Any new xlang runtime should follow these rules even if its surface API looks
different:
+
+1. Keep root operations on the runtime facade and nested payload work on
explicit read and write
+ contexts.
+2. Keep reference tracking behind dedicated read-side and write-side services
so the disabled path
+ stays cheap.
+3. Make serializers payload-only. Type metadata, registration, and root
framing belong to the
+ runtime and type resolver layers.
+4. Track per-operation state explicitly. Do not rely on ambient thread-local
runtime state.
+5. Reserve read reference IDs before materializing new objects, and bind
partially built objects as
+ soon as a nested child may refer back to them.
+6. Keep meta-share session state explicit and resettable.
+7. Preserve the separation between the root bitmap, per-object ref flags, type
headers, and
+ payload bytes.
+8. After any xlang protocol change, run the cross-language test matrix and
update both this guide
+ and [Xlang Serialization Spec](xlang_serialization_spec.md).
diff --git a/docs/specification/xlang_serialization_spec.md
b/docs/specification/xlang_serialization_spec.md
index 084bc6cd9..da344a902 100644
--- a/docs/specification/xlang_serialization_spec.md
+++ b/docs/specification/xlang_serialization_spec.md
@@ -1229,9 +1229,12 @@ The implementation can accumulate read count with map
size to decide whether to
### enum
-Enums are serialized as an unsigned var int. If the order of enum values
change, the deserialized enum value may not be
-the value users expect. In such cases, users must register enum serializer by
make it write enum value as an enumerated
-string with unique hash disabled.
+Enums are serialized as an unsigned var int tag. For plain enums, this tag is
typically the
+declaration ordinal. Some implementations or generated enum forms may instead
use an explicit
+stable enum value or variant ID. If the encoding relies on declaration order,
reordering enum
+values can change the deserialized result. In such cases, users should prefer
an explicit stable
+ID-based encoding or register a custom enum serializer that writes a stable
string representation
+with unique hash disabled.
### timestamp
diff --git a/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
b/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
index cff610a81..56695d535 100644
--- a/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
+++ b/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
@@ -332,7 +332,6 @@ public sealed class RoundtripTests
private static ForyRuntime BuildFory(bool compatible, bool trackRef)
{
return ForyRuntime.Builder()
- .Xlang(true)
.Compatible(compatible)
.TrackRef(trackRef)
.Build();
diff --git
a/java/fory-core/src/main/java/org/apache/fory/annotation/ForyEnumId.java
b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyEnumId.java
new file mode 100644
index 000000000..01e695165
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyEnumId.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares a stable numeric enum tag for Fory serialization.
+ *
+ * <p>Apply this annotation to exactly one enum id field, exactly one
zero-argument enum id method,
+ * or every enum constant field. When absent, Fory falls back to the
declaration ordinal.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+public @interface ForyEnumId {
+ /**
+ * Explicit enum tag for enum-constant annotations.
+ *
+ * <p>Leave the default {@code -1} when annotating an enum id field or
getter.
+ */
+ int value() default -1;
+}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
index e88681be5..c72bb10b5 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
@@ -19,9 +19,15 @@
package org.apache.fory.serializer;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
+import org.apache.fory.annotation.ForyEnumId;
+import org.apache.fory.collection.LongMap;
import org.apache.fory.config.Config;
import org.apache.fory.context.ReadContext;
import org.apache.fory.context.WriteContext;
@@ -29,23 +35,20 @@ import org.apache.fory.util.Preconditions;
@SuppressWarnings("rawtypes")
public class EnumSerializer extends ImmutableSerializer<Enum> implements
Shareable {
+ private static final int MAX_ENUM_ID_ARRAY_SIZE = 2048;
+
private final Config config;
private final Enum[] enumConstants;
private final Map<String, Enum> stringToEnum;
+ private final int[] tagByOrdinal;
+ private final Enum[] enumConstantByTagArray;
+ private final LongMap<Enum> enumConstantByTagMap;
public EnumSerializer(Config config, Class<Enum> cls) {
super(config, cls, false);
this.config = config;
- if (cls.isEnum()) {
- enumConstants = cls.getEnumConstants();
- } else {
- Preconditions.checkArgument(Enum.class.isAssignableFrom(cls) && cls !=
Enum.class);
- @SuppressWarnings("unchecked")
- Class<Enum> enclosingClass = (Class<Enum>) cls.getEnclosingClass();
- Preconditions.checkNotNull(enclosingClass);
- Preconditions.checkArgument(enclosingClass.isEnum());
- enumConstants = enclosingClass.getEnumConstants();
- }
+ Class<Enum> enumClass = resolveEnumClass(cls);
+ enumConstants = enumClass.getEnumConstants();
if (config.serializeEnumByName()) {
stringToEnum = new HashMap<>();
for (Enum enumConstant : enumConstants) {
@@ -54,6 +57,10 @@ public class EnumSerializer extends
ImmutableSerializer<Enum> implements Shareab
} else {
stringToEnum = null;
}
+ EnumTagCodec tagCodec = EnumTagCodec.build(enumClass, enumConstants);
+ tagByOrdinal = tagCodec.tagByOrdinal;
+ enumConstantByTagArray = tagCodec.enumConstantByTagArray;
+ enumConstantByTagMap = tagCodec.enumConstantByTagMap;
}
@Override
@@ -61,7 +68,7 @@ public class EnumSerializer extends ImmutableSerializer<Enum>
implements Shareab
if (!config.isXlang() && config.serializeEnumByName()) {
writeContext.writeString(value.name());
} else {
- writeContext.getBuffer().writeVarUint32Small7(value.ordinal());
+
writeContext.getBuffer().writeVarUint32Small7(tagByOrdinal[value.ordinal()]);
}
}
@@ -75,15 +82,21 @@ public class EnumSerializer extends
ImmutableSerializer<Enum> implements Shareab
}
return handleUnknownEnumValue(name);
} else {
- int value = readContext.getBuffer().readVarUint32Small7();
- if (value >= enumConstants.length) {
- return handleUnknownEnumValue(value);
+ int tag = readContext.getBuffer().readVarUint32Small7();
+ Enum value = null;
+ if (enumConstantByTagArray != null && tag <
enumConstantByTagArray.length) {
+ value = enumConstantByTagArray[tag];
+ } else if (enumConstantByTagMap != null) {
+ value = enumConstantByTagMap.get(tag);
+ }
+ if (value != null) {
+ return value;
}
- return enumConstants[value];
+ return handleUnknownEnumValue(tag);
}
}
- private Enum handleUnknownEnumValue(int value) {
+ private Enum handleUnknownEnumValue(int tag) {
switch (config.getUnknownEnumValueStrategy()) {
case RETURN_NULL:
return null;
@@ -93,7 +106,7 @@ public class EnumSerializer extends
ImmutableSerializer<Enum> implements Shareab
return enumConstants[enumConstants.length - 1];
default:
throw new IllegalArgumentException(
- String.format("Enum ordinal %s not in %s", value,
Arrays.toString(enumConstants)));
+ String.format("Enum tag %s not in %s", tag,
Arrays.toString(enumConstants)));
}
}
@@ -110,4 +123,207 @@ public class EnumSerializer extends
ImmutableSerializer<Enum> implements Shareab
String.format("Enum string %s not in %s", value,
Arrays.toString(enumConstants)));
}
}
+
+ private static Class<Enum> resolveEnumClass(Class<Enum> cls) {
+ if (cls.isEnum()) {
+ return cls;
+ }
+ Preconditions.checkArgument(Enum.class.isAssignableFrom(cls) && cls !=
Enum.class);
+ @SuppressWarnings("unchecked")
+ Class<Enum> enclosingClass = (Class<Enum>) cls.getEnclosingClass();
+ Preconditions.checkNotNull(enclosingClass);
+ Preconditions.checkArgument(enclosingClass.isEnum());
+ return enclosingClass;
+ }
+
+ private interface EnumIdAccessor {
+ int getId(Enum value);
+ }
+
+ private static final class EnumTagCodec {
+ private final int[] tagByOrdinal;
+ private final Enum[] enumConstantByTagArray;
+ private final LongMap<Enum> enumConstantByTagMap;
+
+ private EnumTagCodec(
+ int[] tagByOrdinal, Enum[] enumConstantByTagArray, LongMap<Enum>
enumConstantByTagMap) {
+ this.tagByOrdinal = tagByOrdinal;
+ this.enumConstantByTagArray = enumConstantByTagArray;
+ this.enumConstantByTagMap = enumConstantByTagMap;
+ }
+
+ private static EnumTagCodec build(Class<Enum> enumClass, Enum[]
enumConstants) {
+ EnumIdAccessor accessor = resolveEnumIdAccessor(enumClass);
+ if (accessor == null) {
+ int[] tagByOrdinal = new int[enumConstants.length];
+ for (int i = 0; i < enumConstants.length; i++) {
+ tagByOrdinal[i] = i;
+ }
+ return new EnumTagCodec(tagByOrdinal, enumConstants, null);
+ }
+ return buildExplicitCodec(enumClass, enumConstants, accessor);
+ }
+
+ private static EnumTagCodec buildExplicitCodec(
+ Class<Enum> enumClass, Enum[] enumConstants, EnumIdAccessor accessor) {
+ int[] tagByOrdinal = new int[enumConstants.length];
+ LongMap<Enum> enumConstantByTag = new LongMap<>(enumConstants.length);
+ int maxTag = 0;
+ for (Enum enumConstant : enumConstants) {
+ int tag = accessor.getId(enumConstant);
+ Enum previous = enumConstantByTag.put(tag, enumConstant);
+ if (previous != null) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Enum %s reuses Fory enum id %s for %s and %s",
+ enumClass.getName(), tag, previous.name(),
enumConstant.name()));
+ }
+ tagByOrdinal[enumConstant.ordinal()] = tag;
+ if (tag > maxTag) {
+ maxTag = tag;
+ }
+ }
+ if (maxTag < MAX_ENUM_ID_ARRAY_SIZE) {
+ Enum[] enumConstantByTagArray = new Enum[maxTag + 1];
+ enumConstantByTag.forEach((tag, value) ->
enumConstantByTagArray[tag.intValue()] = value);
+ return new EnumTagCodec(tagByOrdinal, enumConstantByTagArray, null);
+ }
+ return new EnumTagCodec(tagByOrdinal, null, enumConstantByTag);
+ }
+
+ private static EnumIdAccessor resolveEnumIdAccessor(Class<Enum> enumClass)
{
+ Map<String, Integer> constantIds = new HashMap<>();
+ Field idField = null;
+ Method idMethod = null;
+ int enumConstantFieldCount = 0;
+ for (Field field : enumClass.getDeclaredFields()) {
+ ForyEnumId annotation = field.getAnnotation(ForyEnumId.class);
+ if (field.isEnumConstant()) {
+ enumConstantFieldCount++;
+ if (annotation != null) {
+ Preconditions.checkArgument(
+ annotation.value() >= 0,
+ "Enum %s constant %s annotated with @ForyEnumId must declare a
non-negative value",
+ enumClass.getName(),
+ field.getName());
+ constantIds.put(field.getName(), annotation.value());
+ }
+ } else if (annotation != null) {
+ Preconditions.checkArgument(
+ annotation.value() == -1,
+ "Enum %s field %s annotated with @ForyEnumId must not declare an
explicit value",
+ enumClass.getName(),
+ field.getName());
+ Preconditions.checkArgument(
+ idField == null,
+ "Enum %s has multiple fields annotated with @ForyEnumId",
+ enumClass.getName());
+ idField = field;
+ }
+ }
+ for (Method method : enumClass.getDeclaredMethods()) {
+ ForyEnumId annotation = method.getAnnotation(ForyEnumId.class);
+ if (annotation != null) {
+ Preconditions.checkArgument(
+ annotation.value() == -1,
+ "Enum %s method %s annotated with @ForyEnumId must not declare
an explicit value",
+ enumClass.getName(),
+ method.getName());
+ Preconditions.checkArgument(
+ idMethod == null,
+ "Enum %s has multiple methods annotated with @ForyEnumId",
+ enumClass.getName());
+ idMethod = method;
+ }
+ }
+
+ if (!constantIds.isEmpty()) {
+ Preconditions.checkArgument(
+ constantIds.size() == enumConstantFieldCount,
+ "Enum %s must annotate every enum constant with @ForyEnumId when
any enum constant uses it",
+ enumClass.getName());
+ Preconditions.checkArgument(
+ idField == null && idMethod == null,
+ "Enum %s must use exactly one @ForyEnumId strategy",
+ enumClass.getName());
+ return value -> constantIds.get(value.name());
+ }
+
+ if (idField == null && idMethod == null) {
+ return null;
+ }
+ Preconditions.checkArgument(
+ idField == null || idMethod == null,
+ "Enum %s must use exactly one @ForyEnumId strategy",
+ enumClass.getName());
+ if (idField != null) {
+ Field field = idField;
+ Preconditions.checkArgument(
+ !Modifier.isStatic(field.getModifiers()),
+ "Enum %s field %s annotated with @ForyEnumId must not be static",
+ enumClass.getName(),
+ field.getName());
+ field.setAccessible(true);
+ return value -> readFieldId(enumClass, field, value);
+ }
+ Method method = idMethod;
+ Preconditions.checkArgument(
+ !Modifier.isStatic(method.getModifiers()),
+ "Enum %s method %s annotated with @ForyEnumId must not be static",
+ enumClass.getName(),
+ method.getName());
+ Preconditions.checkArgument(
+ Modifier.isPublic(method.getModifiers()),
+ "Enum %s method %s annotated with @ForyEnumId must be public",
+ enumClass.getName(),
+ method.getName());
+ Preconditions.checkArgument(
+ method.getParameterCount() == 0,
+ "Enum %s method %s annotated with @ForyEnumId must not take
arguments",
+ enumClass.getName(),
+ method.getName());
+ method.setAccessible(true);
+ return value -> invokeMethodId(enumClass, method, value);
+ }
+
+ private static int readFieldId(Class<Enum> enumClass, Field field, Enum
value) {
+ try {
+ return extractTag(enumClass, field.getName(), field.get(value));
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Failed to read @ForyEnumId field %s on enum %s",
+ field.getName(), enumClass.getName()),
+ e);
+ }
+ }
+
+ private static int invokeMethodId(Class<Enum> enumClass, Method method,
Enum value) {
+ try {
+ return extractTag(enumClass, method.getName(), method.invoke(value));
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Failed to invoke @ForyEnumId method %s on enum %s",
+ method.getName(), enumClass.getName()),
+ e);
+ }
+ }
+
+ private static int extractTag(Class<Enum> enumClass, String source, Object
tagValue) {
+ Preconditions.checkArgument(
+ tagValue instanceof Number,
+ "Enum %s @ForyEnumId source %s must return a numeric value",
+ enumClass.getName(),
+ source);
+ long tag = ((Number) tagValue).longValue();
+ Preconditions.checkArgument(
+ tag >= 0 && tag <= Integer.MAX_VALUE,
+ "Enum %s @ForyEnumId source %s returned out-of-range id %s",
+ enumClass.getName(),
+ source,
+ tag);
+ return (int) tag;
+ }
+ }
}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/serializer/EnumSerializerTest.java
b/java/fory-core/src/test/java/org/apache/fory/serializer/EnumSerializerTest.java
index 7dfebd82d..aaa117965 100644
---
a/java/fory-core/src/test/java/org/apache/fory/serializer/EnumSerializerTest.java
+++
b/java/fory-core/src/test/java/org/apache/fory/serializer/EnumSerializerTest.java
@@ -21,15 +21,18 @@ package org.apache.fory.serializer;
import static org.testng.Assert.*;
+import java.lang.reflect.Field;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.fory.Fory;
import org.apache.fory.ForyTestBase;
+import org.apache.fory.annotation.ForyEnumId;
import org.apache.fory.codegen.JaninoUtils;
import org.apache.fory.config.CompatibleMode;
import org.apache.fory.config.ForyBuilder;
import org.apache.fory.config.Language;
import org.apache.fory.exception.DeserializationException;
+import org.apache.fory.memory.MemoryBuffer;
import org.testng.annotations.Test;
public class EnumSerializerTest extends ForyTestBase {
@@ -55,6 +58,95 @@ public class EnumSerializerTest extends ForyTestBase {
abstract void f();
}
+ public enum EnumWithIdField {
+ A(10),
+ B(20),
+ C(30);
+
+ @ForyEnumId private final int id;
+
+ EnumWithIdField(int id) {
+ this.id = id;
+ }
+ }
+
+ public enum EnumWithIdMethod {
+ A(100),
+ B(200),
+ C(300);
+
+ private final int id;
+
+ EnumWithIdMethod(int id) {
+ this.id = id;
+ }
+
+ @ForyEnumId
+ public int getId() {
+ return id;
+ }
+ }
+
+ public enum EnumWithConstantIds {
+ @ForyEnumId(3)
+ A,
+ @ForyEnumId(7)
+ B,
+ @ForyEnumId(11)
+ C
+ }
+
+ public enum EnumWithLargeIds {
+ A(4096),
+ B(8192);
+
+ private final int id;
+
+ EnumWithLargeIds(int id) {
+ this.id = id;
+ }
+
+ @ForyEnumId
+ public int getId() {
+ return id;
+ }
+ }
+
+ public enum EnumWithPartialConstantIds {
+ @ForyEnumId(1)
+ A,
+ B
+ }
+
+ public enum EnumWithDuplicateIds {
+ A(1),
+ B(1);
+
+ private final int id;
+
+ EnumWithDuplicateIds(int id) {
+ this.id = id;
+ }
+
+ @ForyEnumId
+ public int getId() {
+ return id;
+ }
+ }
+
+ public enum EnumWithConflictingIdStrategies {
+ @ForyEnumId(1)
+ A(10),
+ @ForyEnumId(2)
+ B(20);
+
+ @ForyEnumId private final int id;
+
+ EnumWithConflictingIdStrategies(int id) {
+ this.id = id;
+ }
+ }
+
@Test(dataProvider = "crossLanguageReferenceTrackingConfig")
public void testEnumSerialization(boolean referenceTracking, Language
language) {
ForyBuilder builder =
@@ -84,6 +176,76 @@ public class EnumSerializerTest extends ForyTestBase {
copyCheckWithoutSame(fory, EnumSubClass.B);
}
+ @Test
+ public void testEnumSerializationUsesOrdinalArrayByDefault() throws
Exception {
+ Fory fory = Fory.builder().requireClassRegistration(false).build();
+ EnumSerializer serializer = getEnumSerializer(fory, EnumFoo.class);
+
+ assertEquals(writeEnumTag(fory, serializer, EnumFoo.B), 1);
+ assertNotNull(readPrivateField(serializer, "enumConstantByTagArray"));
+ assertNull(readPrivateField(serializer, "enumConstantByTagMap"));
+ }
+
+ @Test
+ public void testEnumSerializationUsesAnnotatedFieldId() {
+ Fory fory = Fory.builder().requireClassRegistration(false).build();
+ EnumSerializer serializer = getEnumSerializer(fory, EnumWithIdField.class);
+
+ assertEquals(writeEnumTag(fory, serializer, EnumWithIdField.B), 20);
+ assertEquals(serDe(fory, fory, EnumWithIdField.C), EnumWithIdField.C);
+ }
+
+ @Test
+ public void testEnumSerializationUsesAnnotatedMethodId() {
+ Fory fory = Fory.builder().requireClassRegistration(false).build();
+ EnumSerializer serializer = getEnumSerializer(fory,
EnumWithIdMethod.class);
+
+ assertEquals(writeEnumTag(fory, serializer, EnumWithIdMethod.B), 200);
+ assertEquals(serDe(fory, fory, EnumWithIdMethod.A), EnumWithIdMethod.A);
+ }
+
+ @Test
+ public void testEnumSerializationUsesAnnotatedConstantId() {
+ Fory fory = Fory.builder().requireClassRegistration(false).build();
+ EnumSerializer serializer = getEnumSerializer(fory,
EnumWithConstantIds.class);
+
+ assertEquals(writeEnumTag(fory, serializer, EnumWithConstantIds.B), 7);
+ assertEquals(serDe(fory, fory, EnumWithConstantIds.C),
EnumWithConstantIds.C);
+ }
+
+ @Test
+ public void testEnumSerializationUsesSparseMapForLargeIds() throws Exception
{
+ Fory fory = Fory.builder().requireClassRegistration(false).build();
+ EnumSerializer serializer = getEnumSerializer(fory,
EnumWithLargeIds.class);
+
+ assertEquals(writeEnumTag(fory, serializer, EnumWithLargeIds.B), 8192);
+ assertNull(readPrivateField(serializer, "enumConstantByTagArray"));
+ assertNotNull(readPrivateField(serializer, "enumConstantByTagMap"));
+ }
+
+ @Test
+ public void testEnumSerializationRejectsPartialConstantIds() {
+ Fory fory = Fory.builder().requireClassRegistration(false).build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> getEnumSerializer(fory, EnumWithPartialConstantIds.class));
+ }
+
+ @Test
+ public void testEnumSerializationRejectsDuplicateIds() {
+ Fory fory = Fory.builder().requireClassRegistration(false).build();
+ assertThrows(
+ IllegalArgumentException.class, () -> getEnumSerializer(fory,
EnumWithDuplicateIds.class));
+ }
+
+ @Test
+ public void testEnumSerializationRejectsConflictingIdStrategies() {
+ Fory fory = Fory.builder().requireClassRegistration(false).build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> getEnumSerializer(fory, EnumWithConflictingIdStrategies.class));
+ }
+
@Test
public void testEnumSerializationUnexistentEnumValueAsNull() {
String enumCode2 = "enum TestEnum2 {" + " A;" + "}";
@@ -286,4 +448,26 @@ public class EnumSerializerTest extends ForyTestBase {
.build(),
new EnumSubclassFieldTest(EnumSubClass.B));
}
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static EnumSerializer getEnumSerializer(Fory fory, Class<? extends
Enum> enumClass) {
+ return (EnumSerializer) fory.getSerializer((Class) enumClass);
+ }
+
+ private static int writeEnumTag(Fory fory, EnumSerializer serializer, Enum
value) {
+ MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(16);
+ try {
+ fory.getWriteContext().prepare(buffer, null);
+ serializer.write(fory.getWriteContext(), value);
+ return buffer.readVarUint32Small7();
+ } finally {
+ fory.getWriteContext().reset();
+ }
+ }
+
+ private static Object readPrivateField(Object target, String fieldName)
throws Exception {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field.get(target);
+ }
}
diff --git a/java/fory-core/src/test/java/org/apache/fory/xlang/EnumTest.java
b/java/fory-core/src/test/java/org/apache/fory/xlang/EnumTest.java
index 551d91858..f190bc0b7 100644
--- a/java/fory-core/src/test/java/org/apache/fory/xlang/EnumTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/xlang/EnumTest.java
@@ -20,6 +20,7 @@
package org.apache.fory.xlang;
import org.apache.fory.Fory;
+import org.apache.fory.annotation.ForyEnumId;
import org.apache.fory.config.CompatibleMode;
import org.apache.fory.config.Language;
import org.apache.fory.config.UnknownEnumValueStrategy;
@@ -47,6 +48,48 @@ public class EnumTest {
Color2 color;
}
+ enum StableColor {
+ Green(10),
+ Red(20),
+ Blue(30),
+ White(40);
+
+ private final int id;
+
+ StableColor(int id) {
+ this.id = id;
+ }
+
+ @ForyEnumId
+ public int getId() {
+ return id;
+ }
+ }
+
+ enum StableColor2 {
+ Red(20),
+ Green(10);
+
+ private final int id;
+
+ StableColor2(int id) {
+ this.id = id;
+ }
+
+ @ForyEnumId
+ public int getId() {
+ return id;
+ }
+ }
+
+ static class StableEnumWrapper {
+ StableColor color;
+ }
+
+ static class StableEnumWrapper2 {
+ StableColor2 color;
+ }
+
@Test
public void testEnumEnum() {
Fory fory1 =
@@ -88,4 +131,52 @@ public class EnumTest {
EnumWrapper2 wrapper3 = (EnumWrapper2) fory3.deserialize(serialize);
Assert.assertEquals(wrapper3.color, Color2.Red);
}
+
+ @Test
+ public void testEnumById() {
+ Fory fory1 =
+ Fory.builder()
+ .withLanguage(Language.XLANG)
+ .withCompatibleMode(CompatibleMode.COMPATIBLE)
+ .withCodegen(false)
+ .build();
+ fory1.register(StableColor.class, 201);
+ fory1.register(StableColor2.class, 202);
+ fory1.register(StableEnumWrapper.class, 203);
+ Fory fory2 =
+ Fory.builder()
+ .withLanguage(Language.XLANG)
+ .withCompatibleMode(CompatibleMode.COMPATIBLE)
+
.withUnknownEnumValueStrategy(UnknownEnumValueStrategy.RETURN_FIRST_VARIANT)
+ .withCodegen(false)
+ .build();
+ fory2.register(StableColor.class, 201);
+ fory2.register(StableColor2.class, 202);
+ fory2.register(StableEnumWrapper2.class, 203);
+
+ StableEnumWrapper knownValue = new StableEnumWrapper();
+ knownValue.color = StableColor.Red;
+ StableEnumWrapper2 decodedKnown =
+ (StableEnumWrapper2) fory2.deserialize(fory1.serialize(knownValue));
+ Assert.assertEquals(decodedKnown.color, StableColor2.Red);
+
+ StableEnumWrapper unknownValue = new StableEnumWrapper();
+ unknownValue.color = StableColor.White;
+ byte[] serializedUnknown = fory1.serialize(unknownValue);
+ StableEnumWrapper2 decodedUnknown = (StableEnumWrapper2)
fory2.deserialize(serializedUnknown);
+ Assert.assertEquals(decodedUnknown.color, StableColor2.Red);
+
+ Fory fory3 =
+ Fory.builder()
+ .withLanguage(Language.XLANG)
+ .withCompatibleMode(CompatibleMode.COMPATIBLE)
+
.withUnknownEnumValueStrategy(UnknownEnumValueStrategy.RETURN_LAST_VARIANT)
+ .withCodegen(false)
+ .build();
+ fory3.register(StableColor.class, 201);
+ fory3.register(StableColor2.class, 202);
+ fory3.register(StableEnumWrapper2.class, 203);
+ StableEnumWrapper2 decodedLast = (StableEnumWrapper2)
fory3.deserialize(serializedUnknown);
+ Assert.assertEquals(decodedLast.color, StableColor2.Green);
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]