Skip to main content
Version: dev

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 serializerTarget
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