跳到主要内容
版本:dev

外部类型序列化

外部类型序列化为其他 Dart 库或包拥有的类生成 Fory struct 序列化器。请定义包含相同字段的本地序列化器声明,并使用 ForyStruct.target 指定外部类。

定义外部结构化序列化器

假设某个依赖拥有以下类:

final class User {
const User({
required this.name,
required this.age,
});

final String name;
final int age;
}

在包中添加序列化器声明:

import 'package:fory/fory.dart';
import 'package:third_party/models.dart' as third_party;

part 'external_serializers.fory.dart';

(target: third_party.User)
abstract final class UserSerializer {
(id: 1)
late final String name;

(id: 2, type: Int32Type())
late final int age;
}

声明必须是 abstract final,不能包含类型参数,并且必须将每个 Schema 字段声明为不带初始化器的 late final。每个序列化字段的名称和 Dart 类型(包括可空性和泛型参数)都必须与目标上可访问的 getter 完全匹配。

声明的字段列表就是完整的外部 Schema。Fory 不会自动添加目标、其父类或其 mixin 中的字段。可以像声明其他 Schema 字段一样,显式声明可访问的继承目标属性。exposePrivateFieldsignoreInheritedPrivateFields 仅适用于普通 Dart 继承层次,不能与 ForyStruct.target 一起使用。

公共目标字段会自动计入对象图内存预算,但该计算不会将它们添加到序列化 Schema。请在额外声明字段上使用 @ForyField(ignore: true),以便将其他存储计入预算而不序列化。

照常运行生成器:

dart run build_runner build

注册并使用目标

通过生成模块注册外部目标:

final fory = Fory();
ExternalSerializersForyModule.register(
fory,
third_party.User,
name: 'example.User',
);

final bytes = fory.serialize(
const third_party.User(name: 'Ada', age: 36),
);
final user = fory.deserialize<third_party.User>(bytes);

数字 ID 的用法相同:

ExternalSerializersForyModule.register(
fory,
third_party.User,
id: 100,
);

注册目标 third_party.User,不要注册 UserSerializer

构造函数和可变目标

未指定 constructor 选项时,代码生成使用目标的公共未命名生成式构造函数。构造函数参数按名称映射到 Schema 字段,并且必须使用完全相同的 Dart 类型。

例如,假设依赖公开以下不可变类:

final class Money {
const Money.fromParts({
required this.currency,
required this.units,
});

final String currency;
final int units;
}

在序列化器声明中选择其公共具名生成式构造函数:

(
target: third_party.Money,
constructor: 'fromParts',
)
abstract final class MoneySerializer {
late final String currency;

(type: Int64Type())
late final int units;
}

解码字段后,生成的序列化器会调用 third_party.Money.fromParts(currency: ..., units: ...)

对于可变目标,使用不含必需参数的公共生成式构造函数并提供匹配 setter,Fory 就能先构造目标,再为字段赋值。启用引用跟踪时,这也支持循环引用。例如,假设依赖公开:

final class Node {
Node.empty();

late String label;
Node? next;
}

在序列化器声明中选择 Node.empty

(
target: third_party.Node,
constructor: 'empty',
)
abstract final class NodeSerializer {
late final String label;

(ref: true)
late final third_party.Node? next;
}

生成的序列化器可以调用 third_party.Node.empty(),发布新节点用于引用跟踪,然后为 labelnext 赋值。

基于构造函数的目标无法解码静态已知、启用引用跟踪并返回自身的路径,因为读取完构造函数参数之前目标并不存在。这包括将目标嵌套为 ListSet 元素,或作为 Map key 或 value。代码生成会拒绝这些 Schema。需要循环引用时,请使用可变两阶段目标或自定义序列化器。对于基于构造函数的目标,也不支持无法从声明中确定的间接循环。

工厂构造函数、私有构造函数、抽象目标、外部 enum、外部 union、record、扩展类型和内置集合类型不能作为外部 struct 目标。

字段和集合

注册后,普通已注册 struct 可以使用的任何位置都可以使用该目标。包含它的生成类无需选择序列化器声明:

()
final class Group {
Group();

third_party.User? owner;

(element: DeclaredType())
List<third_party.User> users = <third_party.User>[];

(value: DeclaredType())
Map<String, third_party.User> usersByName =
<String, third_party.User>{};
}

嵌套 list、set 和 map 会递归解析已注册目标。动态字段和异构集合也会按注册类型解析每个具体目标:

(dynamic: true)
Object? value;

注册每个可能动态出现的具体外部类型。

非空根 list、set 和 map 会将其元素、key 和 value 解码为已注册外部目标。Dart 根集合保留现有运行时结构,因此请将根集合作为 Object? 读取,再转换其外层载体:

final decoded =
fory.deserialize<Object?>(
fory.serialize(<third_party.User>[user]),
)
as List<Object?>;
final first = decoded.first as third_party.User;

空根集合不包含元素类型标识。

封闭泛型目标

假设依赖公开以下泛型类:

final class Box<T> {
const Box(this.value);

final T value;
}

Box<T> 是开放类型,因为 T 尚未解析。提供 Box<String> 等具体类型参数后,会生成代码生成可以分析的封闭泛型实例:

(target: third_party.Box<String>)
abstract final class StringBoxSerializer {
late final String value;
}

生成的序列化器使用 third_party.Box<String>(...) 重建值,并且只适用于 Box<String>。请直接注册和使用该精确目标类型:

final fory = Fory();
ExternalSerializersForyModule.register(
fory,
third_party.Box<String>,
id: 102,
);

final input = const third_party.Box<String>('hello');
final bytes = fory.serialize(input);
final output = fory.deserialize<third_party.Box<String>>(bytes);

print(output.value); // hello

注册 third_party.Box<String>,不要注册 StringBoxSerializerBox<int> 等其他实例需要自己的序列化器声明和注册;一个声明不会覆盖每个 Box<T>

Schema 演进

evolving 和字段 ID 的工作方式与普通生成 struct 完全相同:

(
target: third_party.User,
evolving: true,
)
abstract final class UserSerializer {
(id: 1)
late final String name;
}

跨通信方保持字段 ID 和已注册类型标识稳定。字段名称仍必须与本地目标类上的对应属性匹配。

何时使用自定义序列化器

当目标需要自定义编码主体、字段名称转换、值转换、只能通过工厂构造、私有状态,或任何无法通过匹配公共 getter、构造函数参数和 setter 表达的重建规则时,请使用自定义序列化器

相关主题