Skip to main content
Version: 1.5

Polymorphic Serialization

Apache Fory™ supports polymorphic serialization through smart pointers (std::shared_ptr and std::unique_ptr), enabling dynamic dispatch and type flexibility for inheritance hierarchies.

Supported Polymorphic Types​

  • std::shared_ptr<Base> - Shared ownership with polymorphic dispatch
  • std::unique_ptr<Base> - Exclusive ownership with polymorphic dispatch
  • Collections: std::vector<std::shared_ptr<Base>>, std::map<K, std::unique_ptr<Base>>
  • Optional: std::optional<std::shared_ptr<Base>>

Basic Polymorphic Serialization​

#include "fory/serialization/fory.h"

using namespace fory::serialization;

// Define base class with virtual methods
struct Animal {
virtual ~Animal() = default;
virtual std::string speak() const = 0;
int32_t age = 0;
};
FORY_STRUCT(Animal, age);

// Define derived classes
struct Dog : Animal {
std::string speak() const override { return "Woof!"; }
std::string breed;
};
FORY_STRUCT(Dog, FORY_BASE(Animal), breed);

struct Cat : Animal {
std::string speak() const override { return "Meow!"; }
std::string color;
};
FORY_STRUCT(Cat, FORY_BASE(Animal), color);

// Struct with polymorphic field
struct Zoo {
std::shared_ptr<Animal> star_animal;
};
FORY_STRUCT(Zoo, star_animal);

int main() {
auto fory = Fory::builder().xlang(true).track_ref(true).build();

// Register all types with unique type IDs
fory.register_struct<Zoo>(100);
fory.register_struct<Dog>(101);
fory.register_struct<Cat>(102);

// Create object with polymorphic field
Zoo zoo;
zoo.star_animal = std::make_shared<Dog>();
zoo.star_animal->age = 3;
static_cast<Dog*>(zoo.star_animal.get())->breed = "Labrador";

// Serialize
auto bytes_result = fory.serialize(zoo);
assert(bytes_result.ok());

// Deserialize - concrete type is preserved
auto decoded_result = fory.deserialize<Zoo>(bytes_result.value());
assert(decoded_result.ok());

auto decoded = std::move(decoded_result).value();
assert(decoded.star_animal->speak() == "Woof!");
assert(decoded.star_animal->age == 3);

auto* dog_ptr = dynamic_cast<Dog*>(decoded.star_animal.get());
if (dog_ptr == nullptr || dog_ptr->breed != "Labrador") {
return 1;
}
}

Type Registration for Polymorphism​

For polymorphic serialization, register derived types with unique type IDs:

// Register with numeric type ID
fory.register_struct<Derived1>(100);
fory.register_struct<Derived2>(101);

Why type ID registration?

  • Compact binary representation
  • Fast type lookup and dispatch
  • Consistent with non-polymorphic type registration

Automatic Polymorphism Detection​

Fory automatically detects polymorphic types using std::is_polymorphic<T>:

struct Base {
virtual ~Base() = default; // Virtual destructor makes it polymorphic
int32_t value = 0;
};

struct NonPolymorphic {
int32_t value = 0; // No virtual methods
};

// Polymorphic field - type info written automatically
struct Container1 {
std::shared_ptr<Base> ptr; // Auto-detected as polymorphic
};

// Non-polymorphic field - no type info written
struct Container2 {
std::shared_ptr<NonPolymorphic> ptr; // Not polymorphic
};

Controlling Dynamic Dispatch​

Use fory::F().dynamic(V) in FORY_STRUCT to override automatic polymorphism detection:

struct Animal {
virtual ~Animal() = default;
virtual std::string speak() const = 0;
};

struct Pet {
// Auto-detected: type info written (Animal has virtual methods)
std::shared_ptr<Animal> animal1;

// Force dynamic: type info written explicitly
std::shared_ptr<Animal> animal2;

// Force non-dynamic: skip type info (faster but no subtype support)
std::shared_ptr<Animal> animal3;
};
FORY_STRUCT(Pet, animal1, (animal2, fory::F().dynamic(true)),
(animal3, fory::F().dynamic(false)));

When to use dynamic(false):

  • You know the concrete type will always match the declared type
  • Performance is critical and you don't need subtype support
  • Working with monomorphic data despite having a polymorphic base class

Schema Metadata​

Configure field metadata directly in FORY_STRUCT:

struct Zoo {
std::shared_ptr<Animal> star; // Auto-detected as polymorphic
std::shared_ptr<Animal> backup; // Nullable polymorphic field
std::shared_ptr<Animal> mascot; // Non-dynamic (no subtype dispatch)
};
FORY_STRUCT(Zoo, (star, fory::F(0)),
(backup, fory::F(1).nullable()),
(mascot, fory::F(2).dynamic(false)));

See Schema Metadata for complete details on nullable(), ref(), and other field-level options.

std::unique_ptr Polymorphism​

std::unique_ptr works the same way as std::shared_ptr for polymorphic types:

struct Container {
std::unique_ptr<Animal> pet;
};
FORY_STRUCT(Container, pet);

auto fory = Fory::builder().xlang(true).track_ref(true).build();
fory.register_struct<Container>(200);
fory.register_struct<Dog>(201);

Container container;
container.pet = std::make_unique<Dog>();
static_cast<Dog*>(container.pet.get())->breed = "Beagle";

auto bytes = fory.serialize(container).value();
auto decoded = fory.deserialize<Container>(bytes).value();

// Runtime type preserved
auto* dog = dynamic_cast<Dog*>(decoded.pet.get());
assert(dog != nullptr);
assert(dog->breed == "Beagle");

Collections of Polymorphic Objects​

#include <vector>
#include <map>

struct AnimalShelter {
std::vector<std::shared_ptr<Animal>> animals;
std::map<std::string, std::unique_ptr<Animal>> registry;
};
FORY_STRUCT(AnimalShelter, animals, registry);

auto fory = Fory::builder().xlang(true).track_ref(true).build();
fory.register_struct<AnimalShelter>(100);
fory.register_struct<Dog>(101);
fory.register_struct<Cat>(102);

AnimalShelter shelter;
shelter.animals.push_back(std::make_shared<Dog>());
shelter.animals.push_back(std::make_shared<Cat>());
shelter.registry["pet1"] = std::make_unique<Dog>();

auto bytes = fory.serialize(shelter).value();
auto decoded = fory.deserialize<AnimalShelter>(bytes).value();

// All concrete types preserved
assert(dynamic_cast<Dog*>(decoded.animals[0].get()) != nullptr);
assert(dynamic_cast<Cat*>(decoded.animals[1].get()) != nullptr);
assert(dynamic_cast<Dog*>(decoded.registry["pet1"].get()) != nullptr);

Reference Tracking​

Reference tracking for std::shared_ptr works the same with polymorphic types. See Supported Types for details and examples.

Nested Polymorphism Depth Limit​

To prevent stack overflow from deeply nested polymorphic structures, Fory limits the maximum dynamic nesting depth:

struct Container {
virtual ~Container() = default;
int32_t value = 0;
std::shared_ptr<Container> nested;
};
FORY_STRUCT(Container, value, nested);

// Default max_dyn_depth is 5
auto fory1 = Fory::builder().xlang(true).build();
assert(fory1.config().max_dyn_depth == 5);

// Increase limit for deeper nesting
auto fory2 = Fory::builder().xlang(true).max_dyn_depth(10).build();
fory2.register_struct<Container>(1);

// Create deeply nested structure
auto level3 = std::make_shared<Container>();
level3->value = 3;

auto level2 = std::make_shared<Container>();
level2->value = 2;
level2->nested = level3;

auto level1 = std::make_shared<Container>();
level1->value = 1;
level1->nested = level2;

// Serialization succeeds
auto bytes = fory2.serialize(level1).value();

// Deserialization succeeds with sufficient depth
auto decoded = fory2.deserialize<std::shared_ptr<Container>>(bytes).value();

Depth exceeded error:

auto fory_shallow = Fory::builder().xlang(true).max_dyn_depth(2).build();
fory_shallow.register_struct<Container>(1);

// 3 levels exceeds max_dyn_depth=2
auto result = fory_shallow.deserialize<std::shared_ptr<Container>>(bytes);
assert(!result.ok()); // Fails with depth exceeded error

When to adjust:

  • Increase max_dyn_depth: For legitimate deeply nested polymorphic data structures
  • Decrease max_dyn_depth: For stricter security requirements or shallow data structures

Nullability for Polymorphic Fields​

By default, std::shared_ptr<T> and std::unique_ptr<T> fields are treated as non-nullable in the schema. To allow nullptr, mark the field nullable in FORY_STRUCT.

struct Pet {
// Non-nullable (default)
std::shared_ptr<Animal> primary;

// Nullable via explicit field metadata
std::shared_ptr<Animal> optional;
};
FORY_STRUCT(Pet, primary, (optional, fory::F().nullable()));

See Schema Metadata for more details.

Combining Polymorphism with Other Features​

Polymorphism + Reference Tracking​

struct GraphNode {
virtual ~GraphNode() = default;
int32_t id = 0;
std::vector<std::shared_ptr<GraphNode>> neighbors;
};
FORY_STRUCT(GraphNode, id, neighbors);

struct WeightedNode : GraphNode {
double weight = 0.0;
};
FORY_STRUCT(WeightedNode, FORY_BASE(GraphNode), weight);

// Enable ref tracking to handle shared references and cycles
auto fory = Fory::builder().xlang(true).track_ref(true).build();
fory.register_struct<GraphNode>(100);
fory.register_struct<WeightedNode>(101);

// Create cyclic graph
auto node1 = std::make_shared<WeightedNode>();
node1->id = 1;

auto node2 = std::make_shared<WeightedNode>();
node2->id = 2;

node1->neighbors.push_back(node2);
node2->neighbors.push_back(node1); // Cycle

auto bytes = fory.serialize(node1).value();
auto decoded = fory.deserialize<std::shared_ptr<GraphNode>>(bytes).value();
// Cycle handled correctly

Polymorphism + Schema Evolution​

Compatible mode is enabled by default for schema evolution with polymorphic types:

auto fory = Fory::builder()
.xlang(true)
.track_ref(true)
.build();

Best Practices​

  1. Use type ID registration for polymorphic types:

    fory.register_struct<DerivedType>(100);
  2. Enable reference tracking for polymorphic types:

    auto fory = Fory::builder().xlang(true).track_ref(true).build();
  3. Virtual destructors required: Ensure base classes have virtual destructors:

    struct Base {
    virtual ~Base() = default; // Required for polymorphism
    };
  4. Declare every serialized base relationship with FORY_BASE:

    FORY_STRUCT(DerivedType, FORY_BASE(BaseType), derived_field);

    This lets Fory validate polymorphic values and correctly adjust pointers for multiple inheritance during deserialization. A derived value whose base relationship is not declared cannot be deserialized through that base smart pointer type.

  5. Register all concrete types before serialization/deserialization:

    fory.register_struct<Derived1>(100);
    fory.register_struct<Derived2>(101);
  6. Use dynamic_cast to downcast after deserialization:

    auto* derived = dynamic_cast<DerivedType*>(base_ptr.get());
    if (derived) {
    // Use derived-specific members
    }
  7. Adjust max_dyn_depth based on your data structure depth:

    auto fory = Fory::builder().xlang(true).max_dyn_depth(10).build();
  8. Use nullable() for optional polymorphic fields:

    FORY_STRUCT(Holder, (optional_ptr, fory::F().nullable()));

Error Handling​

auto bytes_result = fory.serialize(obj);
if (!bytes_result.ok()) {
std::cerr << "Serialization failed: "
<< bytes_result.error().to_string() << std::endl;
return;
}

auto decoded_result = fory.deserialize<MyType>(bytes_result.value());
if (!decoded_result.ok()) {
std::cerr << "Deserialization failed: "
<< decoded_result.error().to_string() << std::endl;
return;
}

Common errors:

  • Type not registered: Register all concrete types with unique IDs before use
  • Depth exceeded: Increase max_dyn_depth for deeply nested structures
  • Type ID conflict: Ensure each type has a unique type ID across all registered types

Performance Considerations​

Polymorphic serialization overhead:

  • Type metadata written for each polymorphic object (~16-32 bytes)
  • Dynamic type resolution during deserialization
  • Virtual function calls for dynamic dispatch

Optimization tips:

  1. Use dynamic(false) when the concrete type matches the declared type:

    FORY_STRUCT(Holder, (fixed_type, fory::F().dynamic(false)));
  2. Minimize nesting depth to reduce metadata overhead

  3. Batch polymorphic objects in collections rather than individual fields

  4. Consider non-polymorphic alternatives when polymorphism isn't needed:

    std::variant<Dog, Cat> animal; // Type-safe union instead of polymorphism