title: Polymorphic Serialization sidebar_position: 5 id: polymorphism 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
Apache Fory™ supports polymorphic serialization through smart pointers (std::shared_ptr and std::unique_ptr), enabling dynamic dispatch and type flexibility for inheritance hierarchies.
std::shared_ptr<Base> - Shared ownership with polymorphic dispatchstd::unique_ptr<Base> - Exclusive ownership with polymorphic dispatchstd::vector<std::shared_ptr<Base>>, std::map<K, std::unique_ptr<Base>>std::optional<std::shared_ptr<Base>>#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, age, breed); struct Cat : Animal { std::string speak() const override { return "Meow!"; } std::string color; }; FORY_STRUCT(Cat, age, color); // Struct with polymorphic field struct Zoo { std::shared_ptr<Animal> star_animal; }; FORY_STRUCT(Zoo, star_animal); int main() { auto fory = Fory::builder().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 - runtime 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()); assert(dog_ptr != nullptr); assert(dog_ptr->breed == "Labrador"); }
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?
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 };
Use fory::dynamic<V> 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 fory::field<std::shared_ptr<Animal>, 0, fory::dynamic<true>> animal2; // Force non-dynamic: skip type info (faster but no runtime subtyping) fory::field<std::shared_ptr<Animal>, 1, fory::dynamic<false>> animal3; }; FORY_STRUCT(Pet, animal1, animal2, animal3);
When to use fory::dynamic<false>:
Use FORY_FIELD_CONFIG to configure fields without fory::field<> wrapper:
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, backup, mascot); // Configure fields with tag IDs and options FORY_FIELD_CONFIG(Zoo, (star, fory::F(0)), // Tag ID 0, default options (backup, fory::F(1).nullable()), // Tag ID 1, allow nullptr (mascot, fory::F(2).dynamic(false)) // Tag ID 2, disable polymorphism );
See Field Configuration for complete details on fory::nullable, fory::ref, and other field-level options
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().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");
#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().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 runtime 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 for std::shared_ptr works the same with polymorphic types. See Supported Types for details and examples.
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().build(); assert(fory1.config().max_dyn_depth == 5); // Increase limit for deeper nesting auto fory2 = Fory::builder().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().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:
max_dyn_depth: For legitimate deeply nested polymorphic data structuresmax_dyn_depth: For stricter security requirements or shallow data structuresBy default, std::shared_ptr<T> and std::unique_ptr<T> fields are treated as non-nullable in the schema. To allow nullptr, wrap the field with fory::field<> (or FORY_FIELD_TAGS) and opt in with fory::nullable.
struct Pet { // Non-nullable (default) std::shared_ptr<Animal> primary; // Nullable via explicit field metadata fory::field<std::shared_ptr<Animal>, 0, fory::nullable> optional; }; FORY_STRUCT(Pet, primary, optional);
See Field Configuration for more details.
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, id, neighbors, weight); // Enable ref tracking to handle shared references and cycles auto fory = Fory::builder().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
Use compatible mode for schema evolution with polymorphic types:
auto fory = Fory::builder() .compatible(true) // Enable schema evolution .track_ref(true) .build();
Use type ID registration for polymorphic types:
fory.register_struct<DerivedType>(100);
Enable reference tracking for polymorphic types:
auto fory = Fory::builder().track_ref(true).build();
Virtual destructors required: Ensure base classes have virtual destructors:
struct Base { virtual ~Base() = default; // Required for polymorphism };
Register all concrete types before serialization/deserialization:
fory.register_struct<Derived1>(100); fory.register_struct<Derived2>(101);
Use dynamic_cast to downcast after deserialization:
auto* derived = dynamic_cast<DerivedType*>(base_ptr.get()); if (derived) { // Use derived-specific members }
Adjust max_dyn_depth based on your data structure depth:
auto fory = Fory::builder().max_dyn_depth(10).build();
Use fory::nullable for optional polymorphic fields:
fory::field<std::shared_ptr<Base>, 0, fory::nullable> optional_ptr;
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:
max_dyn_depth for deeply nested structuresPolymorphic serialization overhead:
Optimization tips:
Use fory::dynamic<false> when runtime type matches declared type:
fory::field<std::shared_ptr<Base>, 0, fory::dynamic<false>> fixed_type;
Minimize nesting depth to reduce metadata overhead
Batch polymorphic objects in collections rather than individual fields
Consider non-polymorphic alternatives when polymorphism isn't needed:
std::variant<Dog, Cat> animal; // Type-safe union instead of polymorphism
max_dyn_depth and other settings