跳到主要内容
版本:dev

Schema 演进

Apache Fory™ 在兼容模式中支持 Schema 演进,允许序列化和反序列化对等端使用不同的类型定义。

兼容模式

兼容模式默认启用:

use fory::Fory;
use fory::ForyStruct;
use std::collections::HashMap;

#[derive(ForyStruct, Debug)]
struct PersonV1 {
name: String,
age: i32,
address: String,
}

#[derive(ForyStruct, Debug)]
struct PersonV2 {
name: String,
age: i32,
// address removed
// phone added
phone: Option<String>,
metadata: HashMap<String, String>,
}

let mut fory1 = Fory::builder().xlang(false).build();
fory1.register::<PersonV1>(1)?;

let mut fory2 = Fory::builder().xlang(false).build();
fory2.register::<PersonV2>(1)?;

let person_v1 = PersonV1 {
name: "Alice".to_string(),
age: 30,
address: "123 Main St".to_string(),
};

// Serialize with V1
let bytes = fory1.serialize(&person_v1)?;

// Deserialize with V2 - missing fields get default values
let person_v2: PersonV2 = fory2.deserialize(&bytes)?;
assert_eq!(person_v2.name, "Alice");
assert_eq!(person_v2.age, 30);
assert_eq!(person_v2.phone, None);

Schema 演进功能

  • 添加带默认值的新字段
  • 删除过时字段(反序列化期间跳过)
  • 更改字段可空性(TOption<T>
  • 重排字段(按名称而非位置匹配)
  • 值可以在不损失精度或范围的情况下转换时,更改部分标量字段类型
  • 对缺失字段以类型安全方式回退到默认值

当值可以无损转换时,兼容读取端可以处理部分标量字段类型变更。如果转换后的值具有相同逻辑值,匹配字段可以在 boolString、数字标量和十进制字段之间读取。例如,"true""false" 可以读取为布尔值,"123" 可以读取为能够保存 123 的数值字段,数字和十进制可以读取为规范字符串;只有不丢失精度或范围时,数字扩宽或缩窄才会成功。数字字符串使用有限 ASCII 十进制语法。可选字段仍可与这些转换组合,但启用引用跟踪的标量类型变更不兼容。无效字符串和有损转换会在反序列化期间失败。

兼容性规则

  • 字段名称必须匹配(区分大小写)
  • 类型变更仅支持可空/不可空变更和部分无损标量转换
  • 嵌套结构体类型必须在两端注册

相同 Schema 优化

只有每个载荷反序列化所用 Schema 始终与序列化时相同,并且希望获得更快序列化和更小体积时,才使用 .compatible(false)。对于跨语言载荷,只有确认每种语言都使用相同 Schema,或原生类型由 Fory Schema IDL 生成时才使用 .compatible(false)

let mut fory = Fory::builder()
.xlang(false)
.compatible(false)
.build();

对于单个结构体,可以使用 #[fory(evolving = false)] 退出演进元数据:

use fory::ForyStruct;

#[derive(ForyStruct)]
#[fory(evolving = false)]
struct SameSchemaMessage {
id: i32,
}

外部类型

外部结构化序列化器是其目标的本地 Schema。兼容模式匹配其声明字段和变体的方式,与等价的本地派生类型完全相同:

#[derive(ForyStruct)]
#[fory(target = third_party::User)]
struct UserSerializer {
name: String,
age: u32,
}

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

let decoded: third_party::User =
fory.deserialize_with::<UserSerializer>(&bytes)?;

对等端必须为逻辑类型使用相同的数字 ID 或注册名称。添加或删除字段遵循常规兼容模式规则。自定义序列化器则拥有不透明 EXT 主体,因此其实现必须在该主体中自行定义版本控制。

枚举支持

Apache Fory™ 在兼容模式中支持三种具有完整 Schema 演进能力的枚举变体:

变体类型:

  • 单元:C 风格枚举(Status::Active
  • 未命名:类似元组的变体(Message::Pair(String, i32)
  • 命名:类似结构体的变体(Event::Click { x: i32, y: i32 }
use fory::{Fory, ForyUnion};

#[derive(ForyUnion, Debug, PartialEq)]
enum Value {
#[fory(default)]
Null,
Bool(bool),
Number(f64),
Text(String),
Object { name: String, value: i32 },
}

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

let value = Value::Object { name: "score".to_string(), value: 100 };
let bytes = fory.serialize(&value)?;
let decoded: Value = fory.deserialize(&bytes)?;
assert_eq!(value, decoded);

对于 case 为 unit 或单载荷变体的类型化 ADT union,如果需要保留未来的载荷变体,请添加 #[fory(unknown)] Unknown(::fory::UnknownCase) 变体。不要将未知变体设为默认值;应保留一个以 #[fory(default)] 标记的真实 Schema case。在反序列化需要保留的未知 case 之前,请先在本地注册未来的载荷类型。

UnknownCase 将其载荷存储为 Arc<dyn Any + Send + Sync>,因此需要保留的载荷类型必须满足 Send + Sync。通过结构化载体注册的直接泛型容器不支持作为擦除的 Any 载荷。请将容器包装在已注册的派生类型中;如果有意使用不透明的 EXT/NAMED_EXT 主体,也可以注册精确目标类型的自定义序列化器。

枚举 Schema 演进

兼容模式通过变体类型编码(2 位)实现稳健的 Schema 演进:

  • 0b0 = Unit,0b1 = 未具名,0b10 = 具名
use fory::{Fory, ForyUnion};

// Old version
#[derive(ForyUnion)]
enum OldEvent {
#[fory(default)]
Click { x: i32, y: i32 },
Scroll { delta: f64 },
}

// New version - added field and variant
#[derive(ForyUnion)]
enum NewEvent {
#[fory(default)]
Unknown,
Click { x: i32, y: i32, timestamp: u64 }, // Added field
Scroll { delta: f64 },
KeyPress(String), // New variant
}

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

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

// Serialize with old schema
let old_bytes = writer.serialize(&OldEvent::Click { x: 100, y: 200 })?;

// Deserialize with new schema - timestamp gets default value (0)
let new_event: NewEvent = reader.deserialize(&old_bytes)?;
assert!(matches!(new_event, NewEvent::Click { x: 100, y: 200, timestamp: 0 }));

演进能力:

  • 未知变体 → 回退到默认变体
  • 具名变体字段 → 添加或删除字段(缺失字段使用默认值)
  • 未具名变体元素 → 添加或删除元素(跳过多余元素,缺失元素使用默认值)
  • 变体类型不匹配 → 自动使用当前变体的默认值

最佳实践:

  • 始终只使用 #[fory(default)] 标记一个 union 变体
  • 具名变体比未具名变体具有更好的演进能力
  • 跨版本通信使用兼容模式

元组支持

Apache Fory™ 开箱即用地支持最多 22 个元素的元组,并在兼容模式和相同 Schema 优化中提供高效序列化。

功能:

  • 自动序列化包含 1 到 22 个元素的元组
  • 支持异构类型(每个元素可以是不同类型)
  • 兼容模式中的 Schema 演进(处理缺失或多余元素)

Schema 模式:

  1. 相同 Schema 优化:不使用集合头部,顺序序列化元素,以降低开销
  2. 兼容模式:使用带类型元数据的集合协议进行 Schema 演进
use fory::{Fory, Error};

let mut fory = Fory::builder().xlang(false).build();

// Tuple with heterogeneous types
let data: (i32, String, bool, Vec<i32>) = (
42,
"hello".to_string(),
true,
vec![1, 2, 3],
);

let bytes = fory.serialize(&data)?;
let decoded: (i32, String, bool, Vec<i32>) = fory.deserialize(&bytes)?;
assert_eq!(data, decoded);

相关主题