跳到主要内容
版本:dev

Schema 元数据

本文说明如何配置 Rust 序列化的字段级元数据。

概述

Apache Fory™ 提供 #[fory(...)] 属性宏,用于在编译期指定可选的字段级元数据。它支持:

  • Tag ID:分配紧凑数字 ID,尽量降低结构体字段元数据大小开销
  • 可空性:控制字段能否为 null
  • 引用跟踪:为共享所有权类型启用引用跟踪
  • 跳过字段:将字段排除在序列化之外
  • 编码控制:指定整数编码方式(varint、fixed、tagged)

基本语法

#[fory(...)] 属性放在各结构体字段上:

use fory::ForyStruct;

#[derive(ForyStruct)]
struct Person {
#[fory(id = 0)]
name: String,

#[fory(id = 1)]
age: i32,

#[fory(id = 2, nullable)]
nickname: Option<String>,
}

多个选项使用逗号分隔。

可用选项

字段 ID(id = N

为字段分配数字 ID,以尽量降低结构体字段元数据大小开销:

#[derive(ForyStruct)]
struct User {
#[fory(id = 0)]
id: i64,

#[fory(id = 1)]
name: String,

#[fory(id = 2)]
age: i32,
}

优势

  • 序列化体积更小(元数据中的数字 ID 相比字段名称)
  • 允许重命名字段而不破坏二进制兼容性

建议:建议为兼容模式配置字段 ID,因为这可以降低序列化成本。

注意

  • ID 在结构体中必须唯一
  • ID 必须为非负数
  • 未指定时在元数据中使用字段名称(开销更大)

跳过字段(skip

将字段排除在序列化之外:

#[derive(ForyStruct)]
struct User {
#[fory(id = 0)]
id: i64,

#[fory(id = 1)]
name: String,

#[fory(skip)]
password: String, // Not serialized
}

password 字段不会包含在序列化输出中,反序列化后保持默认值。

可空性(nullable

控制是否为字段写入 null 标志:

use fory::{Fory, RcWeak};

#[derive(ForyStruct)]
struct Record {
// RcWeak is nullable by default, override to non-nullable
#[fory(id = 0, nullable = false)]
required_ref: RcWeak<Data>,
}

默认行为

类型默认可空
Option<T>true
RcWeak<T>, ArcWeak<T>true
所有其他类型false

注意事项

  • 对于 Option<T>RcWeak<T>ArcWeak<T>,可空性默认为 true
  • 对于所有其他类型,可空性默认为 false
  • 对默认可空的类型使用 nullable = false 覆盖默认设置

引用跟踪(ref

控制共享所有权类型的字段级引用跟踪:

use std::rc::Rc;
use std::sync::Arc;

#[derive(ForyStruct)]
struct Container {
// Enable reference tracking (default for Rc/Arc)
#[fory(id = 0, ref = true)]
shared_data: Rc<Data>,

// Disable reference tracking
#[fory(id = 1, ref = false)]
unique_data: Rc<Data>,
}

默认行为

类型默认引用跟踪
Rc<T>, Arc<T>true
RcWeak<T>, ArcWeak<T>true
Option<Rc<T>>, Option<Arc<T>>true(继承)
所有其他类型false

使用场景

  • 对可能形成循环或被共享的字段启用
  • 对始终唯一的字段禁用(优化)

编码(encoding

控制整数字段的编码方式:

#[derive(ForyStruct)]
struct Metrics {
// Variable-length encoding (smaller for small values)
#[fory(id = 0, encoding = varint)]
count: i64,

// Fixed-length encoding (consistent size)
#[fory(id = 1, encoding = fixed)]
timestamp: i64,

// Tagged encoding (includes type tag, u64 only)
#[fory(id = 2, encoding = tagged)]
value: u64,
}

支持的编码

类型选项默认值
i32, u32varint, fixedvarint
i64, u64varint, fixed, taggedvarint

使用时机

  • varint:最适合通常较小的值(默认)
  • fixed:最适合使用完整范围的值(例如时间戳、哈希)
  • tagged:需要保留类型信息时(仅 u64)

嵌套集合配置

覆盖设置属于嵌套元素而非外层字段时,请使用 list(element(...))map(key(...), value(...))

use std::collections::HashMap;

#[derive(ForyStruct)]
struct Data {
#[fory(list(element(encoding = fixed)))]
fixed_values: Vec<i32>,

#[fory(map(key(encoding = fixed), value(nullable = true, encoding = tagged)))]
values_by_id: HashMap<Option<i32>, Option<u64>>,
}

未加注解的 Vec<i32> 字段是采用默认 varint 元素编码的 list<int32>。上面的 fixed_values 字段仍为 LIST,只将元素编码改为定长 int32。字段 Schema 本身是稠密原始数组时使用 #[fory(array)];二进制数据使用 #[fory(bytes)] 标注 Vec<u8>

compress 已被移除。请直接使用 encoding = varintencoding = fixed

外部类型字段选择

使用 with 选择目标为确切字段类型的序列化器。该序列化器可以是外部结构化序列化器、自定义序列化器或载体组合:

use fory::{ForyStruct, VecSerializer};
use std::collections::HashMap;

#[derive(ForyStruct)]
struct Envelope {
#[fory(with = UserSerializer)]
user: third_party::User,

#[fory(with = VecSerializer<UserSerializer>)]
direct_users: Vec<third_party::User>,

#[fory(list(element(with = UserSerializer)))]
users: Vec<third_party::User>,

#[fory(map(value(with = UserSerializer)))]
by_name: HashMap<String, third_party::User>,

#[fory(tuple(element(index = 1, with = UserSerializer)))]
selected: (String, third_party::User),
}

with 选择目标为确切字段节点的序列化器,因此 direct_users 对整个 Vec 使用载体序列化器。每个递归注解只应用于其子节点。例如,映射键和值可以选择不同序列化器。元组索引从零开始。完整递归语法和支持的载体参见外部类型序列化

类型分类

Fory 对字段类型进行分类以确定默认行为:

类型类别示例默认可空默认引用
Primitivei8, i32, f64, boolfalsefalse
OptionOption<T>truefalse
RcRc<T>falsetrue
ArcArc<T>falsetrue
RcWeakRcWeak<T>(Fory 类型)truetrue
ArcWeakArcWeak<T>(Fory 类型)truetrue
其他StringVec<T>、用户类型falsefalse

特殊情况Option<Rc<T>>Option<Arc<T>> 继承内部类型的引用跟踪行为。

完整示例

use fory::ForyStruct;
use std::rc::Rc;

#[derive(ForyStruct, Default)]
struct Document {
// Required fields with tag IDs
#[fory(id = 0)]
title: String,

#[fory(id = 1)]
version: i32,

// Optional field (nullable by default for Option)
#[fory(id = 2)]
description: Option<String>,

// Reference-tracked shared pointer
#[fory(id = 3)]
parent: Rc<Document>,

// Nullable + reference-tracked
#[fory(id = 4, nullable)]
related: Option<Rc<Document>>,

// Counter with varint encoding (small values)
#[fory(id = 5, encoding = varint)]
view_count: u64,

// Timestamp with fixed encoding (full range values)
#[fory(id = 6, encoding = fixed)]
created_at: i64,

// Skip sensitive field
#[fory(skip)]
internal_state: String,
}

fn main() {
let fory = fory::Fory::builder().xlang(false).build();

let doc = Document {
title: "My Document".to_string(),
version: 1,
description: Some("A sample document".to_string()),
parent: Rc::new(Document::default()),
related: None, // Allowed because nullable
view_count: 42,
created_at: 1704067200,
internal_state: "secret".to_string(), // Will be skipped
};

let bytes = fory.serialize(&doc).unwrap();
let decoded: Document = fory.deserialize(&bytes).unwrap();
}

编译期验证

无效配置会在编译期被发现:

// Error: duplicate field IDs
#[derive(ForyStruct)]
struct Bad {
#[fory(id = 0)]
field1: String,

#[fory(id = 0)] // Compile error: duplicate id
field2: String,
}

// Error: invalid id value
#[derive(ForyStruct)]
struct Bad2 {
#[fory(id = -1)] // Compile error: id must be non-negative
field: String,
}

// Error: invalid encoding for i32
#[derive(ForyStruct)]
struct Bad3 {
#[fory(encoding = tagged)] // Compile error: tagged is only valid for i64/u64
field: i32,
}

跨语言兼容性

序列化要由其他语言(Java、C++、Go、Python)读取的数据时,请使用 Schema 元数据匹配编码预期:

#[derive(ForyStruct)]
struct CrossLangData {
// Matches Java Integer with varint
#[fory(id = 0, encoding = varint)]
int_var: i32,

// Matches Java Integer with fixed
#[fory(id = 1, encoding = fixed)]
int_fixed: i32,

// Matches Java Long with tagged encoding
#[fory(id = 2, encoding = tagged)]
long_tagged: u64,

// Nullable pointer matches Java nullable reference
#[fory(id = 3, nullable)]
optional: Option<String>,
}

Schema 演进

兼容模式支持 Schema 演进。建议配置字段 ID 以降低序列化成本:

// Version 1
#[derive(ForyStruct)]
struct DataV1 {
#[fory(id = 0)]
id: i64,

#[fory(id = 1)]
name: String,
}

// Version 2: Added new field
#[derive(ForyStruct)]
struct DataV2 {
#[fory(id = 0)]
id: i64,

#[fory(id = 1)]
name: String,

#[fory(id = 2)]
email: Option<String>, // New nullable field
}

使用 V1 序列化的数据可以用 V2 反序列化(新字段为 None)。

也可以省略字段 ID(元数据中会使用字段名称,开销更大):

#[derive(ForyStruct)]
struct Data {
id: i64,
name: String,
}

默认值

  • 可空性Option<T>RcWeak<T>ArcWeak<T> 默认可空;所有其他类型不可空
  • 引用跟踪Rc<T>Arc<T>RcWeak<T>ArcWeak<T> 默认启用引用跟踪;所有其他类型禁用

以下情况需要配置字段

  • 字段可以为 None(使用 Option<T>
  • 字段需要跟踪共享或循环对象引用(使用 ref = true
  • 整数类型为跨语言兼容性需要特定编码
  • 希望减小元数据大小(使用字段 ID)
// Xlang mode: explicit configuration required
#[derive(ForyStruct)]
struct User {
#[fory(id = 0)]
name: String, // Non-nullable by default

#[fory(id = 1)]
email: Option<String>, // Nullable (Option<T>)

#[fory(id = 2, ref = true)]
friend: Rc<User>, // Ref tracking (default for Rc)
}

默认值摘要

类型默认可空默认引用跟踪
Primitives, Stringfalsefalse
Option<T>truefalse
Rc<T>, Arc<T>falsetrue
RcWeak<T>, ArcWeak<T>truetrue

最佳实践

  1. 配置字段 ID:建议兼容模式使用,以降低序列化成本
  2. 对敏感数据使用 skip:密码、令牌、内部状态
  3. 对共享对象启用引用跟踪:同一指针多次出现时
  4. 对唯一字段禁用引用跟踪:确定字段唯一时用于优化
  5. 选择合适的编码:小值使用 varint,完整范围值使用 fixed
  6. 保持 ID 稳定:分配后不要更改字段 ID

选项参考

选项语法说明适用于
idid = N用字段标签 ID 减小元数据体积所有字段
skipskip从序列化中排除字段所有字段
nullablenullablenullable = bool控制 null 标志写入所有字段
refrefref = bool控制引用跟踪RcArc、弱引用类型
encodingencoding = varint/fixed/tagged整数编码方式i32u32i64u64
listlist(element(...))元素 Schema 元数据list、set、array
mapmap(key(...), value(...))key/value Schema 元数据HashMapBTreeMap
tupletuple(element(index = N, ...))tuple 位置 Schema 元数据tuple
withwith = Serializer选择字段序列化器外部目标节点

相关主题