Skip to main content
Version: dev

Trait Object Serialization

Apache Fory supports dynamic values through registered application traits and the standard Any trait.

Application Traits

An application trait extends ForyObject. List every concrete target accepted by the trait in register_trait_type!:

use fory::{
register_trait_type, Fory, ForyObject, ForyStruct,
};

trait Animal: ForyObject {
fn name(&self) -> &str;
}

#[derive(ForyStruct)]
struct Dog {
name: String,
}

impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
}

#[derive(ForyStruct)]
struct Cat {
name: String,
}

impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
}

register_trait_type!(Animal, Dog, Cat);

The concrete list contains runtime value types. Each type must also be registered with the Fory instance:

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

A listed third-party target can be registered through an external structural serializer or custom serializer. The list still names the target, not that serializer.

The generated names are private to the macro's module by default. A library that exports the generated root serializers adds normal Rust visibility:

register_trait_type!(pub Animal, Dog, Cat);

The trait and listed types must be visible at least as broadly as the generated serializers. pub(crate) and restricted visibility are also supported.

Trait Object Fields

Box<dyn Trait> and Rc<dyn Trait> work directly in derived fields and nested containers:

use std::collections::HashMap;
use std::rc::Rc;

#[derive(ForyStruct)]
struct Shelter {
featured: Box<dyn Animal>,
shared: Rc<dyn Animal>,
animals: Vec<Box<dyn Animal>>,
by_name: HashMap<String, Rc<dyn Animal>>,
}

Fory checks the closed concrete list before materializing a value. Do not add #[fory(with = ...)] to a trait-object node; its concrete serializer is chosen dynamically from the registered target.

For Arc<dyn Trait>, the trait must be thread-safe and the macro uses its sync form:

use std::sync::Arc;

trait SharedAnimal: ForyObject + Send + Sync {
fn name(&self) -> &str;
}

impl SharedAnimal for Dog {
fn name(&self) -> &str {
&self.name
}
}

impl SharedAnimal for Cat {
fn name(&self) -> &str {
&self.name
}
}

register_trait_type!(sync SharedAnimal, Dog, Cat);

#[derive(ForyStruct)]
struct SharedShelter {
featured: Arc<dyn SharedAnimal>,
}

Every listed target in a sync declaration must implement Send + Sync. Use register_trait_type!(pub sync SharedAnimal, Dog, Cat) when a library exports the generated root serializers.

Trait Object Roots

Box<dyn Trait> is an ordinary root:

let animal: Box<dyn Animal> = Box::new(Dog {
name: "Rex".to_string(),
});

let bytes = fory.serialize(&animal)?;
let decoded: Box<dyn Animal> = fory.deserialize(&bytes)?;
assert_eq!(decoded.name(), "Rex");

Rust's orphan rules prevent an ordinary serializer implementation directly on Rc<dyn Trait> and Arc<dyn Trait>. The macro generates serializer types for those roots:

let animal: Rc<dyn Animal> = Rc::new(Dog {
name: "Milo".to_string(),
});

let bytes =
fory.serialize_with::<AnimalRcSerializer>(&animal)?;
let decoded =
fory.deserialize_with::<AnimalRcSerializer>(&bytes)?;
assert_eq!(decoded.name(), "Milo");
let animal: Arc<dyn SharedAnimal> = Arc::new(Dog {
name: "Luna".to_string(),
});

let bytes =
fory.serialize_with::<SharedAnimalArcSerializer>(&animal)?;
let decoded =
fory.deserialize_with::<SharedAnimalArcSerializer>(&bytes)?;
assert_eq!(decoded.name(), "Luna");

These APIs serialize the original Rc or Arc; there is no conversion wrapper. The generated serializer types are not registered by ID or name.

Dynamic Any

Fory supports:

  • Box<dyn Any>;
  • Rc<dyn Any>;
  • Arc<dyn Any + Send + Sync>.

The concrete target must be registered:

use std::any::Any;

let value: Rc<dyn Any> = Rc::new(Dog {
name: "Rex".to_string(),
});

let bytes = fory.serialize(&value)?;
let decoded: Rc<dyn Any> = fory.deserialize(&bytes)?;
let dog = decoded.downcast_ref::<Dog>().unwrap();
assert_eq!(dog.name, "Rex");

Use Arc<dyn Any + Send + Sync> when the erased value must be shared across threads:

let value: Arc<dyn Any + Send + Sync> = Arc::new(Dog {
name: "Buddy".to_string(),
});

let bytes = fory.serialize(&value)?;
let decoded: Arc<dyn Any + Send + Sync> =
fory.deserialize(&bytes)?;
let dog = decoded.downcast_ref::<Dog>().unwrap();
assert_eq!(dog.name, "Buddy");

Derived serializers support synchronized Arc materialization when their targets satisfy Send + Sync. A custom serializer that needs this path implements read_arc_any.

Generic LIST, SET, and MAP identities do not identify one exact Rust generic target behind Any. Put such a container in a registered struct, or register an exact whole-container custom serializer when an opaque EXT representation is intentional.