Skip to content
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ This is the entry point for AI guidance in Apache Fory. Read this file first, th
- User guide docs must explain user-visible behavior, commands, and examples.
Do not add implementation details, internal ownership rationale, build flags,
or type-id-space caveats unless they directly clarify a confusion users can
act on.
act on. Translate internal owner-model details into concrete user actions, and
avoid phrases such as "serializer-owned capability" or "registration alone
does not..." in user-facing docs.
- Add comments only when behavior is hard to understand or an algorithm is non-obvious.
- Do not remove existing code comments unless they are stale, misleading, redundant, or no longer necessary after the change.
- Only add tests that verify internal behaviors or fix specific bugs; do not create unnecessary tests unless requested.
Expand Down
39 changes: 31 additions & 8 deletions docs/guide/rust/custom-serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ For types that don't support `#[derive(ForyStruct)]`, implement the `Serializer`
## Implementing the Serializer Trait

```rust
use fory::{Fory, ReadContext, WriteContext, Serializer, ForyDefault, Error};
use fory::{Error, Fory, ForyDefault, ReadContext, Serializer, TypeResolver, WriteContext};
use std::any::Any;

#[derive(Debug, PartialEq, Default)]
Expand All @@ -41,20 +41,21 @@ struct CustomType {
}

impl Serializer for CustomType {
fn fory_write_data(&self, context: &mut WriteContext, is_field: bool) {
fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> {
context.writer.write_i32(self.value);
context.writer.write_varuint32(self.name.len() as u32);
context.writer.write_var_u32(self.name.len() as u32);
context.writer.write_utf8_string(&self.name);
Ok(())
}

fn fory_read_data(context: &mut ReadContext, is_field: bool) -> Result<Self, Error> {
let value = context.reader.read_i32();
let len = context.reader.read_varuint32() as usize;
let name = context.reader.read_utf8_string(len);
fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
let value = context.reader.read_i32()?;
let len = context.reader.read_var_u32()? as usize;
let name = context.reader.read_utf8_string(len)?;
Ok(Self { value, name })
}

fn fory_type_id_dyn(&self, type_resolver: &TypeResolver) -> u32 {
fn fory_type_id_dyn(&self, type_resolver: &TypeResolver) -> Result<fory::TypeId, Error> {
Self::fory_get_type_id(type_resolver)
}

Expand All @@ -76,6 +77,28 @@ impl ForyDefault for CustomType {
>
> **Tip**: If your type supports `#[derive(ForyStruct)]`, you can use `#[fory(generate_default)]` to automatically generate both `ForyDefault` and `Default` implementations.

## Manual Serializers and Arc Any

If a manually registered serializer needs its type to round-trip behind
`Arc<dyn Any + Send + Sync>` or preserve `UnknownCase` payloads, implement the
send-sync Any reader and return the concrete value as a boxed `Any` value:

```rust
impl Serializer for CustomType {
fn fory_read_data_as_send_sync_any(
context: &mut ReadContext,
) -> Result<Box<dyn Any + Send + Sync>, Error> {
Ok(Box::new(Self::fory_read_data(context)?))
}

// Implement the ordinary Serializer methods as shown above.
// ...
}
```

Do not override this method for values that contain fields whose types are not
`Send + Sync`, such as `Rc<T>`, `RcWeak<T>`, `RefCell<T>`, or `Cell<T>`.

## Registering Custom Serializers

```rust
Expand Down
5 changes: 4 additions & 1 deletion docs/guide/rust/native-serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ Native serialization owns the Rust-specific object surface:
- `Box<T>`, `Rc<T>`, `Arc<T>`, `RcWeak<T>`, and `ArcWeak<T>`.
- `RefCell<T>` and `Mutex<T>`.
- Trait objects such as `Box<dyn Trait>`, `Rc<dyn Trait>`, and `Arc<dyn Trait>`.
- Runtime type dispatch with `Rc<dyn Any>` and `Arc<dyn Any + Send + Sync>`.
- Runtime type dispatch with `Box<dyn Any>`, `Rc<dyn Any>`, and
`Arc<dyn Any + Send + Sync>` for registered non-container payloads. Wrap
containers in registered structs, enums, or unions before using them behind
erased `Any` carriers.
- Date and time carriers, including optional `chrono` support.

Use [Basic Serialization](basic-serialization.md), [References](references.md), and
Expand Down
46 changes: 43 additions & 3 deletions docs/guide/rust/polymorphism.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ assert_eq!(decoded.star_animal.speak(), "Woof!");

## Serializing dyn Any Trait Objects

Apache Fory™ supports serializing `Rc<dyn Any>` and
Apache Fory™ supports serializing `Box<dyn Any>`, `Rc<dyn Any>`, and
`Arc<dyn Any + Send + Sync>` for runtime type dispatch:

**Key points:**

- Works with any type that implements `Serializer`
- Works with registered concrete non-container types that implement `Serializer`
- Requires downcasting after deserialization to access the concrete type
- Type information is preserved during serialization
- Useful for plugin systems and dynamic type handling
Expand Down Expand Up @@ -132,6 +132,46 @@ let unwrapped = decoded.downcast_ref::<Dog>().unwrap();
assert_eq!(unwrapped.name, "Buddy");
```

`Box<dyn Any>`, `Rc<dyn Any>`, and `Arc<dyn Any + Send + Sync>` are supported
erased `Any` carriers for registered concrete non-container payloads.
Use `Arc<dyn Any + Send + Sync>` when the erased payload must be shareable
across threads; the concrete payload type must also satisfy `Send + Sync`.
Registered structs, enums, and unions that satisfy those bounds can be used as
the erased payload.

The unsupported case is a generic container used directly as the top-level
erased payload. This applies to all erased `Any` carriers: `Box<dyn Any>`,
`Rc<dyn Any>`, and `Arc<dyn Any + Send + Sync>`. Unsupported direct payloads
include list-, map-, and set-like containers such as `Vec<T>`, `Vec<u8>`,
`HashMap<K, V>`, `HashSet<T>`, and `LinkedList<T>`.

If you need to put a container in an erased `Any` payload, wrap it in a
registered struct, enum, or union and use that wrapper as the erased payload:

```rust
use fory::{Fory, ForyStruct};
use std::any::Any;
use std::sync::Arc;

#[derive(ForyStruct)]
struct IntList {
values: Vec<i32>,
}

let mut fory = Fory::builder().xlang(false).build();
fory.register::<IntList>(100)?;

let value: Arc<dyn Any + Send + Sync> = Arc::new(IntList {
values: vec![1, 2, 3],
});
let bytes = fory.serialize(&value)?;
let decoded: Arc<dyn Any + Send + Sync> = fory.deserialize(&bytes)?;
```

The wrapper makes the erased payload a concrete registered type while the
container remains a normal typed field. The same wrapper model is the supported
path for `Box<dyn Any>` and `Rc<dyn Any>`.

## Rc/Arc-Based Trait Objects in Structs

For fields with `Rc<dyn Trait>` or `Arc<dyn Trait>`, Fory automatically handles the conversion:
Expand Down Expand Up @@ -180,7 +220,7 @@ assert_eq!(decoded.animals_arc[0].speak(), "Woof!");

Due to Rust's orphan rule, `Rc<dyn Trait>` and `Arc<dyn Trait>` cannot implement `Serializer` directly. For standalone serialization (not inside struct fields), the `register_trait_type!` macro generates wrapper types.

**Note:** If you don't want to use wrapper types, you can serialize as `Rc<dyn Any>` or `Arc<dyn Any + Send + Sync>` instead (see the dyn Any section above).
**Note:** If you don't want to use wrapper types for concrete non-container payloads, you can serialize as `Box<dyn Any>`, `Rc<dyn Any>`, or `Arc<dyn Any + Send + Sync>` instead (see the dyn Any section above).

The `register_trait_type!` macro generates `AnimalRc` and `AnimalArc` wrapper types:

Expand Down
18 changes: 10 additions & 8 deletions docs/guide/rust/schema-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,16 @@ let decoded: Value = fory.deserialize(&bytes)?;
assert_eq!(value, decoded);
```

For typed ADT unions whose schema cases are unit or single-payload variants,
`#[fory(unknown)] Unknown(::fory::UnknownCase)` is only the runtime
forward-compatibility carrier. It cannot be the default variant, and the union
must include at least one real schema case. The marker only selects the carrier
and does not add an entry to the schema case table; schema cases use
non-negative IDs. `UnknownCase` stores its payload as
`Arc<dyn Any + Send + Sync>`, so locally registered future payload types must be
thread-safe to be preserved as unknown cases.
For typed ADT unions whose cases are unit or single-payload variants, add an
`#[fory(unknown)] Unknown(::fory::UnknownCase)` variant when you need to
preserve future payload variants. Do not make the unknown variant the default;
keep a real schema case marked `#[fory(default)]`. Register future payload types
locally before deserializing unknown cases you need to preserve.

`UnknownCase` stores its payload as `Arc<dyn Any + Send + Sync>`, so preserved
payload types must satisfy `Send + Sync`. Direct generic containers are not
supported as erased `Any` payloads; wrap the container in a registered derived
type if an unknown case needs to preserve it.

### Enum Schema Evolution

Expand Down
12 changes: 12 additions & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,18 @@ The examples in this section use native mode because Rust trait objects and `dyn
- `Box<dyn Any>`/`Rc<dyn Any>`/`Arc<dyn Any + Send + Sync>` - Any trait type objects
- `Vec<Box<dyn Trait>>`, `HashMap<K, Box<dyn Trait>>` - Collections of trait objects

`Box<dyn Any>`, `Rc<dyn Any>`, and `Arc<dyn Any + Send + Sync>` are supported
erased `Any` carriers for registered concrete non-container payloads.
Use `Arc<dyn Any + Send + Sync>` when the erased payload must be shareable
across threads; the concrete payload type must also satisfy `Send + Sync`.
Registered structs, enums, and unions that satisfy those bounds can be used as
the erased payload.
Generic containers such as `Vec<T>`, `HashMap<K, V>`, `HashSet<T>`, and
`LinkedList<T>` are not supported directly as top-level erased `Any` payloads
behind any of those carriers. This also includes primitive vector encodings such
as `Vec<u8>`. Wrap the container in a registered derived type when it needs to
travel behind an erased `Any` carrier.

**Basic Trait Object Serialization Example:**

```rust
Expand Down
12 changes: 12 additions & 0 deletions rust/fory-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,18 @@ impl Error {
}
}

#[cold]
#[inline(never)]
pub(crate) fn unsupported_send_sync_type<T>() -> Error
where
T: ?Sized,
{
Error::type_error(format!(
"{} cannot be represented as Arc<dyn Any + Send + Sync>",
std::any::type_name::<T>()
))
}

/// Ensures a condition is true; otherwise returns an [`enum@Error`].
///
/// # Examples
Expand Down
10 changes: 0 additions & 10 deletions rust/fory-core/src/fory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -954,16 +954,6 @@ impl Fory {
.register_serializer_by_name::<T>(namespace, type_name)
}

/// Registers a generic trait object type for serialization.
/// This method should be used to register collection types such as `Vec<T>`, `HashMap<K, V>`, etc.
/// Don't register concrete struct types with this method. Use `register()` instead.
pub fn register_generic_trait<T: 'static + Serializer + ForyDefault>(
&mut self,
) -> Result<(), Error> {
self.check_registration_allowed()?;
self.type_resolver.register_generic_trait::<T>()
}

/// Writes the serialization header to the writer.
#[inline(always)]
pub fn write_head<T: Serializer>(&self, writer: &mut Writer) {
Expand Down
Loading
Loading