External-Type Serialization
External-type serialization lets an application serialize a type owned by another Swift module without modifying that type.
Use an external structural serializer when the target exposes a schema that Fory can access and construct directly. Use a custom serializer when the target needs a custom encoding or has private construction invariants.
An external structural serializer is a separate declaration. Register it, then select it explicitly at roots, fields, and carrier children.
For one application-owned global implementation, Swift also permits an
external type to conform retroactively to Serializer with Target == Self.
That form uses ordinary implicit selection everywhere and has process-global
conformance risks. See Custom Serializers.
External Structs
Declare a local serializer with the same serialized fields and select the external target:
import Fory
import ThirdParty
@ForyStruct(target: ThirdParty.User.self)
struct UserSerializer {
var name: String
var age: UInt32
}
Register and use the serializer:
let fory = Fory()
try fory.register(UserSerializer.self, id: 100)
let user = ThirdParty.User(name: "Alice", age: 32)
let bytes = try fory.serialize(user, with: UserSerializer.self)
let decoded = try fory.deserialize(bytes, with: UserSerializer.self)
The target struct must expose readable properties and an accessible initializer whose labels and value types match the serializer declaration. A public struct's synthesized memberwise initializer is not automatically public.
External Classes
Use a class serializer declaration for a class target:
@ForyStruct(target: ThirdParty.Node.self)
final class NodeSerializer {
var value: Int32 = 0
@ForyField(with: OptionalSerializer<NodeSerializer>.self)
var next: ThirdParty.Node? = nil
}
The target class must have an accessible zero-argument initializer and writable serialized properties. These requirements allow Fory to preserve shared and circular references.
Swift budgets only fields listed in this declaration. Add
@ForyField(ignore: true) fields for substantial omitted storage; they count
toward the graph budget but are not serialized.
Use a custom serializer if the class is immutable, requires constructor arguments, or cannot be safely observed before all fields are assigned.
External Enums
Use @ForyEnum(target:) for an enum with no associated values:
@ForyEnum(target: ThirdParty.Status.self)
enum StatusSerializer {
case active
case disabled
}
An enum from another module must be exhaustively switchable. A resilient non-frozen public enum requires a custom serializer.
External Unions
Use @ForyUnion(target:) for xlang union values:
@ForyUnion(target: ThirdParty.Command<UnknownCase>.self)
enum CommandSerializer {
@ForyUnknownCase
case unknown(UnknownCase)
@ForyCase(id: 0)
case rename(String)
@ForyCase(id: 1, payload: .with(UserSerializer.self))
case replace(ThirdParty.User)
}
The target must expose matching cases and a lossless
unknown(UnknownCase) case. A dependency-free third-party module can declare a
generic unknown payload, such as Command<UnknownPayload>, and the serializer
can target its Command<UnknownCase> specialization. Use a custom serializer
when a third-party union has a different unknown-case representation or cannot
preserve unknown payloads.
A known Swift union case has zero or one associated value. Use an explicit struct payload when an alternative has multiple logical fields.
Selecting a Serializer for a Field
Select one exact field node with @ForyField(with:):
@ForyStruct
struct Account {
@ForyField(with: UserSerializer.self)
var owner: ThirdParty.User
}
For an optional field, select the optional carrier:
@ForyField(with: OptionalSerializer<UserSerializer>.self)
var owner: ThirdParty.User?
The selected serializer's Target must exactly match the declared field type.
Selecting Serializers Inside Carriers
Use .with(...) inside collection field annotations:
@ForyStruct
struct Directory {
@ListField(element: .with(UserSerializer.self))
var users: [ThirdParty.User]
@SetField(element: .with(KeySerializer.self))
var keys: Set<ThirdParty.Key>
@MapField(
key: .with(KeySerializer.self),
value: .with(UserSerializer.self)
)
var usersByKey: [ThirdParty.Key: ThirdParty.User]
@MapField(
value: .list(element: .with(UserSerializer.self))
)
var groups: [String: [ThirdParty.User]]
}
Whole-carrier selection is equivalent:
@ForyField(with: ArraySerializer<UserSerializer>.self)
var users: [ThirdParty.User]
At one field node, with cannot be combined with an encoding or another type
selection. It may be combined with id.
Root Carrier Composition
Swift provides carrier serializers for its supported generic carriers:
| Carrier serializer | Target |
|---|---|
OptionalSerializer<S> | S.Target? |
ArraySerializer<S> | [S.Target] |
SetSerializer<S> | Set<S.Target> |
DictionarySerializer<KS, VS> | [KS.Target: VS.Target] |
Carrier serializers compose recursively:
typealias DirectorySerializer = DictionarySerializer<
String,
ArraySerializer<OptionalSerializer<UserSerializer>>
>
let bytes = try fory.serialize(
directory,
with: DirectorySerializer.self
)
let decoded = try fory.deserialize(
bytes,
with: DirectorySerializer.self
)
SetSerializer requires a hashable target element.
DictionarySerializer requires a hashable target key.
Carrier serializers use the same optional, array, set, and dictionary encodings as ordinary Swift values.
Buffer APIs
The explicit serializer selection is available for all typed root forms:
var output = Data()
try fory.serialize(user, with: UserSerializer.self, to: &output)
let input = ByteBuffer(data: output)
let decoded = try fory.deserialize(
from: input,
with: UserSerializer.self
)
The with label always selects a serializer.
Dynamic Values
Registered external targets work through dynamic Any, AnyObject, and
application protocol values:
protocol Animal {
var name: String { get }
}
let animal: any Animal = cat
let bytes = try fory.serialize(
animal,
with: DynamicSerializer<any Animal>.self
)
let decoded = try fory.deserialize(
bytes,
with: DynamicSerializer<any Animal>.self
)
Register each concrete target through its ordinary, external structural, or custom serializer.
For a root carrier containing protocol values, compose
DynamicSerializer explicitly:
typealias AnimalArraySerializer =
ArraySerializer<DynamicSerializer<any Animal>>
let bytes = try fory.serialize(
animals,
with: AnimalArraySerializer.self
)
Type Aliases
Serializer aliases and root carrier aliases are supported.
When a field type alias hides a collection shape, supply the full recursive field hint:
typealias Users = [ThirdParty.User]
@ForyField(type: .list(element: .with(UserSerializer.self)))
var users: Users