TL;DR:Apache Fory Schema IDL 是首个面向对象图序列化的跨语言 IDL。你只需在 .fdl 文件中定义一次类型,编译器就能为 Java、Python、Go、Rust、C++、C#、Swift 等语言生成符合各语言习惯的领域对象,并在 Schema 模型中原生支持共享引用、循环、多态、Schema 演进和可选类型。
- GitHub: https://github.com/apache/fory
- 文档: https://fory.apache.org/docs/compiler
- 安装:
pip install fory-compiler
对象图语义的长期缺口
现有大多数序列化 IDL 都把数据建模成值树:消息是扁平的,Schema 层面没有共享身份、循环关系或可复用独立多态类型的概念。当真实数据本质上是图结构时,这个缺口通常会集中暴 露在三个地方:
-
共享引用与循环引用:如果两个字段指向同一个逻辑对象,Protocol Buffers 和 FlatBuffers 不会在 Schema 或编码格式中保留这种共享身份。父指针、DAG 和循环都没有 Schema 级表达方式,最后只能退化成手工 ID 字段和应用侧重建逻辑。
-
多态:Protobuf 的
oneof和 FlatBuffers 的union都是嵌在外层消息中的内联备选项,不是可复用的独立 Schema 类型。Protobuf 的Any虽然支持开放式多态,但仅限消息类型,而且通过 type URL 来表达。FlatBuffers 没有对应能力。 -
生成类型能否直接作为领域模型:FlatBuffers API 主要是面向 buffer 访问的包装器。Protobuf 在许多语言里生成的也是偏传输模型优先的类型,所以用户通常还要再加一层转换,才能回到符合语言习惯的领域对象。Schema 定义了编码格式,但没有真正定义应用模型。
Apache Fory Schema IDL 正是在补上这块空白。
Apache Fory Schema IDL 不同在哪里?
Apache Fory Schema IDL 从一开始就把对象图当成一等公民。你只需定义一次类型,包括共享引用、循环结构、独立 union 和多态字段,运行编译器后,就能在所有已支持语言上得到建立在同一 Fory 编码格式之上的原生代码。
在本文里,“面向对象图的序列化 IDL”指的是:Schema 本身就能直接描述共享身份、循环和可复用的多态类型,而不是强迫用户先把一切压平成值树,再通过手工 ID 或应用层约定把关联关系补回来。
这一点主要体现在三个方面:
ref让共享引用和循环引用成为 Schema 契约的一部分。union和any让多态成为可复用的 Schema 能力,而不只是内联的传输细节。- 生成代码可以直接作为宿主语言的应用模型使用,而不是还要再包一层转换器的包装类型。
下面分节看这些能力在实际里是怎么工作的。
原生支持的共享引用
Fory IDL 提供 ref 修饰符,让共享引用和循环引用在 Schema 中显式可见:
message TreeNode {
string value = 1;
ref TreeNode parent = 2; // 共享引用,可回指父节点
list<ref TreeNode> children = 3; // 每个子节点都启用引用跟踪
}
当你序列 化一棵“子节点会回指父节点”的树时,Fory 会只编码一次每个对象,对重复出现的对象使用回溯引用。无需手工维护 ID 链接字段,也无需在应用层额外重建对象图。对象图契约直接由 Schema 表达。
对于需要打破所有权循环的父指针,ref(weak=true) 会生成弱指针类型,例如 Rust 中的 ArcWeak<Node>、C++ 中的 std::weak_ptr。
生成的是领域对象,不是包装器
Fory .fdl Schema 编译后的一个关键差异,是生成的宿主语言模型可以直接使用,而不是像 Protocol Buffers 或 FlatBuffers 那样更偏底层包装:
- Java:带
@ForyField注解的普通 POJO,可直接用于 Spring、Hibernate 等框架 - Python:带标准类型标注的
@dataclass - Go:带
fory:"id=..."struct tag 的结构体 - Rust:带
#[derive(ForyObject)]的结构体 - C++:带
FORY_STRUCT宏的final类,零运行时反射 - C#:带
[ForyObject]特性的类 - Swift:带
@ForyObject和@ForyField元数据的模型
在很多应用里,你根本不需要再写一层适配层。生成类型本身就可以直接成为领域对象。
内建 union(和类型)
Fory IDL 提供一等公民的 union 结构,并把它映射为各语言最符合习惯的和类型表达:
message Dog {
string name = 1;
int32 bark_volume = 2;
}
message Cat {
string name = 1;
int32 lives = 2;
}
union Animal {
Dog dog = 1;
Cat cat = 2;
}
这会生成:
- Rust:
enum Animal { Dog(Dog), Cat(Cat) } - C++:基于
std::variant的包装器,提供is_dog()、as_dog()、visit()API - Swift:带关联值的带标签枚举
- Java:带类型化 case 访问器的
Union子类 - Python:带
is_dog()/dog_value()辅助方法的Union子类 - Go:带
AsDog()/ visitor 模式的类型化 case 结构 - C#:带
IsDog/DogValue()辅助方法的Union子类
所有语言共享同一组语义,但呈现方式都遵循各自语言的习惯。
用 any 表达多态字段
有些时候,你在定义 Schema 时并不知道字段的具体类型。比如事件总线要承载异构载荷,或者插件系统要接收用户自定义消息类型。Fory IDL 的 any 正是为此设计的,它会把运行时类型身份写入二进制流,并在另一端解析出来:
message Envelope {
string event_type = 1;
any payload = 2; // 可承载 Fory 支持的动态值
}
在运行时,payload 可以持有 Fory 支持的动态值,包括其他生成消息、内建标量类型和集合类型。序列化后的字节会包含运行时类型元信息,因此反序列化器可以在另一端恢复出具体值:
| 语言 | 生成字段类型 |
|---|---|
| Java | Object payload |
| Python | payload: Any |
| Go | Payload any |
| Rust | payload: Box<dyn Any> |
| C++ | std::any payload |
| C# | object Payload |
| Swift | var payload: Any |
这提供了类似 Protobuf Any 的灵活性,但直接体现在 Fory 生成模型里,而且不要求在 Schema 表面引入 Protobuf 风格的 type URL。
上面这三项能力,也就是 ref、union / any 和原生生成代码,让 Fory IDL 成为真正面向对象图的 Schema 语言。Schema 演进是另一回事,但它补齐了生产环境最关键的最后一环:
开箱即用的 Schema 演进
可以加字段,可以删字段,可以独立发布。在兼容模式下,字段按 field id 匹配,缺失字段使用默认值,未知字段会被跳过:
// 版本 1:已经部署到生产
message User {
string name = 1;
int32 age = 2;
}
// 版本 2:另一团队新增字段
message User {
string name = 1;
int32 age = 2;
optional string email = 3; // 新增字段,V1 消费者可安全忽略
}
这依然要遵守兼容性规则,并不是允许你任意修改 Schema。但对于常见的新增字段和删除字段场景,你不需要协调一次大爆炸式发布,也不需要再加一层版本协商机制。
完整示例
下面我们用一个更贴近 真实业务的电商 Schema,看看它如何在当前 Fory 已支持的语言里工作,再用“共享客户对象往返序列化”把对象图能力直观展示出来。
1. 定义 Schema
创建 ecommerce.fdl:
package ecommerce;
enum OrderStatus {
PENDING = 0;
CONFIRMED = 1;
SHIPPED = 2;
DELIVERED = 3;
}
message Address {
string street = 1;
string city = 2;
string country = 3;
}
message Customer {
string id = 1;
string name = 2;
optional string email = 3;
optional Address address = 4;
}
message OrderItem {
string sku = 1;
int32 quantity = 2;
float64 unit_price = 3;
}
message Order {
string order_id = 1;
ref Customer customer = 2;
list<OrderItem> items = 3;
OrderStatus status = 4;
float64 total = 5;
optional string notes = 6;
timestamp created_at = 7;
}
message OrderBatch {
list<Order> orders = 1;
}
2. 安装编译器并生成代码
pip install fory-compiler
# 一条命令为当前 Fory IDL 支持的全部语言生成代码
foryc ecommerce.fdl \
--java_out=./java/src/main/java \
--python_out=./python/gen \
--go_out=./go/gen \
--rust_out=./rust/gen \
--cpp_out=./cpp/gen \
--csharp_out=./csharp/gen \
--swift_out=./swift/gen
一条命令即可同时生成多种语言的代码。注册辅助方法、字节辅助方法和 type ID 都会自动生成。
3. 使用生成代码
Java - 序列化订单:
import ecommerce.*;
Order order = new Order();
order.setOrderId("ORD-2026-001");
Customer customer = new Customer();
customer.setName("Alice");
customer.setEmail("alice@example.com");
order.setCustomer(customer);
order.setStatus(OrderStatus.CONFIRMED);
order.setTotal(259.98);
// 自动生成 toBytes() / fromBytes(),无需手写 Fory 样板代码
byte[] bytes = order.toBytes();
Order restored = Order.fromBytes(bytes);
Python - 反序列化同一份字节:
from ecommerce import Order
# from_bytes() 会处理注册与反序列化
order = Order.from_bytes(bytes_from_java)
print(f"{order.order_id}: {order.customer.name} - ${order.total}")
# ORD-2026-001: Alice - $259.98
Go - 处理订单:
import "gen/ecommerce"
var order ecommerce.Order
if err := order.FromBytes(bytesFromJava); err != nil {
panic(err)
}
fmt.Printf("%s: %s - $%.2f\n", order.OrderId, order.Customer.Name, order.Total)
Rust - 类型安全反序列化:
use gen::ecommerce::Order;
let order = Order::from_bytes(&bytes_from_java)?;
println!("{}: {} - ${:.2}", order.order_id, order.customer.name, order.total);
C++ - 零额外开销访问:
#include "gen/ecommerce.h"
auto order = ecommerce::Order::from_bytes(bytes_from_java).value();
std::cout << order.order_id() << ": " << order.customer().name()
<< " - $" << order.total() << std::endl;
C# - 强类型反序列化:
using Ecommerce;
var order = Order.FromBytes(bytesFromJava);
Console.WriteLine($"{order.OrderId}: {order.Customer.Name} - ${order.Total}");
Swift - 符合语言习惯的模型访问:
import Ecommerce
let order = try Order.fromBytes(bytesFromJava)
print("\(order.orderId): \(order.customer.name) - $\(order.total)")
同一份 Schema 和生成代码可以在所有已支持语言之间产出兼容字节,全程无需手写转换层。
4. 保留共享身份,而不只是值
因为 Order.customer 声明为 ref Customer,共享身份本身就成为 Schema 契约的一部分:
Customer customer = new Customer();
customer.setName("Alice");
Order first = new Order();
first.setOrderId("ORD-1");
first.setCustomer(customer);
Order second = new Order();
second.setOrderId("ORD-2");
second.setCustomer(customer);
OrderBatch batch = new OrderBatch();
batch.setOrders(java.util.Arrays.asList(first, second));
OrderBatch restored = OrderBatch.fromBytes(batch.toBytes());
assert restored.getOrders().get(0).getCustomer()
== restored.getOrders().get(1).getCustomer();
如果换成值树式序列化器,这种共享身份通常要靠你自己重建。而在 Fory IDL 里,ref 直接让它成为 Schema 和生成代码的一部分。
