Thanks to visit codestin.com
Credit goes to fory.apache.org

Skip to main content
Version: dev

Schema Evolution

Apache Fory™ supports schema evolution in Compatible mode, allowing serialization and deserialization peers to have different type definitions.

Compatible Mode

Enable schema evolution with compatible(true):

#include "fory/serialization/fory.h"

using namespace fory::serialization;

// Version 1: Original schema
struct PersonV1 {
std::string name;
int32_t age;
};
FORY_STRUCT(PersonV1, name, age);

// Version 2: Added email field
struct PersonV2 {
std::string name;
int32_t age;
std::string email; // NEW FIELD
};
FORY_STRUCT(PersonV2, name, age, email);

int main() {
// Create separate Fory instances for each schema version
auto fory_v1 = Fory::builder()
.compatible(true) // Enable schema evolution
.xlang(true)
.build();

auto fory_v2 = Fory::builder()
.compatible(true)
.xlang(true)
.build();

// Register with the SAME type ID for schema evolution
constexpr uint32_t PERSON_TYPE_ID = 100;
fory_v1.register_struct<PersonV1>(PERSON_TYPE_ID);
fory_v2.register_struct<PersonV2>(PERSON_TYPE_ID);

// Serialize with V1
PersonV1 v1{"Alice", 30};
auto bytes = fory_v1.serialize(v1).value();

// Deserialize as V2 - email gets default value (empty string)
auto v2 = fory_v2.deserialize<PersonV2>(bytes).value();
assert(v2.name == "Alice");
assert(v2.age == 30);
assert(v2.email == ""); // Default value for missing field

return 0;
}

Schema Evolution Features

Compatible mode supports the following schema changes:

Change TypeSupportBehavior
Add new fieldsMissing fields use default values
Remove fieldsExtra fields are skipped
Reorder fieldsFields matched by name, not position
Change nullabilityTstd::optional<T>
Change field typesTypes must be compatible
Rename fieldsField names must match (case-sensitive)

Adding Fields (Backward Compatibility)

When deserializing old data with a new schema that has additional fields:

// Old schema (V1)
struct ProductV1 {
std::string name;
double price;
};
FORY_STRUCT(ProductV1, name, price);

// New schema (V2) with additional fields
struct ProductV2 {
std::string name;
double price;
std::vector<std::string> tags; // NEW
std::map<std::string, std::string> attributes; // NEW
};
FORY_STRUCT(ProductV2, name, price, tags, attributes);

// Serialize V1
ProductV1 v1{"Laptop", 999.99};
auto bytes = fory_v1.serialize(v1).value();

// Deserialize as V2
auto v2 = fory_v2.deserialize<ProductV2>(bytes).value();
assert(v2.name == "Laptop");
assert(v2.price == 999.99);
assert(v2.tags.empty()); // Default: empty vector
assert(v2.attributes.empty()); // Default: empty map

Removing Fields (Forward Compatibility)

When deserializing new data with an old schema that has fewer fields:

// Full schema
struct UserFull {
int64_t id;
std::string username;
std::string email;
std::string password_hash;
int32_t login_count;
};
FORY_STRUCT(UserFull, id, username, email, password_hash, login_count);

// Minimal schema (removed 3 fields)
struct UserMinimal {
int64_t id;
std::string username;
};
FORY_STRUCT(UserMinimal, id, username);

// Serialize full version
UserFull full{12345, "johndoe", "[email protected]", "hash123", 42};
auto bytes = fory_full.serialize(full).value();

// Deserialize as minimal - extra fields are skipped
auto minimal = fory_minimal.deserialize<UserMinimal>(bytes).value();
assert(minimal.id == 12345);
assert(minimal.username == "johndoe");
// email, password_hash, login_count are skipped

Field Reordering

In compatible mode, fields are matched by name, not by position:

// Original field order
struct ConfigOriginal {
std::string host;
int32_t port;
bool enable_ssl;
std::string protocol;
};
FORY_STRUCT(ConfigOriginal, host, port, enable_ssl, protocol);

// Reordered fields
struct ConfigReordered {
bool enable_ssl; // Moved to first
std::string protocol; // Moved to second
std::string host; // Moved to third
int32_t port; // Moved to last
};
FORY_STRUCT(ConfigReordered, enable_ssl, protocol, host, port);

// Serialize with original order
ConfigOriginal orig{"localhost", 8080, true, "https"};
auto bytes = fory_orig.serialize(orig).value();

// Deserialize with different field order - works correctly
auto reordered = fory_reord.deserialize<ConfigReordered>(bytes).value();
assert(reordered.host == "localhost");
assert(reordered.port == 8080);
assert(reordered.enable_ssl == true);
assert(reordered.protocol == "https");

Nested Struct Evolution

Schema evolution works recursively for nested structs:

// V1 Address
struct AddressV1 {
std::string street;
std::string city;
};
FORY_STRUCT(AddressV1, street, city);

// V2 Address with new fields
struct AddressV2 {
std::string street;
std::string city;
std::string country; // NEW
std::string zipcode; // NEW
};
FORY_STRUCT(AddressV2, street, city, country, zipcode);

// V1 Employee with V1 Address
struct EmployeeV1 {
std::string name;
AddressV1 home_address;
};
FORY_STRUCT(EmployeeV1, name, home_address);

// V2 Employee with V2 Address and new field
struct EmployeeV2 {
std::string name;
AddressV2 home_address; // Nested struct evolved
std::string employee_id; // NEW
};
FORY_STRUCT(EmployeeV2, name, home_address, employee_id);

// Register types with same IDs
constexpr uint32_t ADDRESS_TYPE_ID = 100;
constexpr uint32_t EMPLOYEE_TYPE_ID = 101;

fory_v1.register_struct<AddressV1>(ADDRESS_TYPE_ID);
fory_v1.register_struct<EmployeeV1>(EMPLOYEE_TYPE_ID);
fory_v2.register_struct<AddressV2>(ADDRESS_TYPE_ID);
fory_v2.register_struct<EmployeeV2>(EMPLOYEE_TYPE_ID);

// Serialize V1
EmployeeV1 emp_v1{"Jane Doe", {"123 Main St", "NYC"}};
auto bytes = fory_v1.serialize(emp_v1).value();

// Deserialize as V2
auto emp_v2 = fory_v2.deserialize<EmployeeV2>(bytes).value();
assert(emp_v2.name == "Jane Doe");
assert(emp_v2.home_address.street == "123 Main St");
assert(emp_v2.home_address.city == "NYC");
assert(emp_v2.home_address.country == ""); // Default
assert(emp_v2.home_address.zipcode == ""); // Default
assert(emp_v2.employee_id == ""); // Default

Bidirectional Evolution

Schema evolution works in both directions:

// V2 -> V1 (downgrade)
PersonV2 v2{"Charlie", 35, "[email protected]"};
auto bytes = fory_v2.serialize(v2).value();

auto v1 = fory_v1.deserialize<PersonV1>(bytes).value();
assert(v1.name == "Charlie");
assert(v1.age == 35);
// email field is discarded during deserialization

Default Values

When fields are missing, C++ default initialization is used:

TypeDefault Value
int8_t, int16_t...0
float, double0.0
boolfalse
std::string""
std::vector<T>Empty vector
std::map<K,V>Empty map
std::set<T>Empty set
std::optional<T>std::nullopt
Struct typesDefault-constructed

Schema Consistent Mode (Default)

Without compatible mode, schemas must match exactly:

// Strict mode (default)
auto fory = Fory::builder()
.compatible(false) // Default: schema must match
.xlang(true)
.build();

// Serialization/deserialization requires identical schemas
// Schema mismatches may cause errors or undefined behavior

Use SchemaConsistent mode when:

  • Schemas are guaranteed to match (same binary version)
  • Maximum performance is required (less metadata overhead)
  • You control both serialization and deserialization

Use Compatible mode when:

  • Schemas may evolve independently
  • Cross-version compatibility is required
  • Different services may have different schema versions

Type ID Requirements

For schema evolution to work:

  1. Same Type ID: Different versions of the same struct must use the same type ID
  2. Consistent IDs: Type IDs must be consistent across all Fory instances
  3. Register All Versions: Each Fory instance registers its own struct version
constexpr uint32_t PERSON_TYPE_ID = 100;

// Instance 1 uses PersonV1
fory_v1.register_struct<PersonV1>(PERSON_TYPE_ID);

// Instance 2 uses PersonV2
fory_v2.register_struct<PersonV2>(PERSON_TYPE_ID);

// Same type ID enables schema evolution

Best Practices

1. Plan for Evolution

Design schemas with future changes in mind:

// Good: Use optional for fields that might be removed
struct Config {
std::string host;
int32_t port;
std::optional<std::string> deprecated_field; // Can be removed later
};

2. Use Meaningful Default Values

Consider what default values make sense for new fields:

struct Settings {
int32_t timeout_ms; // Default: 0 (might want a sensible default)
bool enabled; // Default: false
std::string mode; // Default: "" (might want "default")
};

3. Document Schema Versions

Track schema changes for debugging:

// V1: Initial schema (2024-01-01)
// V2: Added email field (2024-02-01)
// V3: Added phone, address fields (2024-03-01)

4. Test Evolution Paths

Test both upgrade and downgrade scenarios:

// Test V1 -> V2
// Test V2 -> V1
// Test V1 -> V3
// Test V3 -> V1

Cross-Language Schema Evolution

Schema evolution works across languages when using xlang mode:

// C++ with compatible mode
auto fory = Fory::builder()
.compatible(true)
.xlang(true)
.build();
// Java with compatible mode
Fory fory = Fory.builder()
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.withLanguage(Language.XLANG)
.build();

Both instances can exchange data even with different schema versions.