Custom Serializers
Use a custom serializer when a target needs a custom encoding or cannot meet the direct-access requirements of an external structural serializer.
A custom serializer is not limited to external types:
- A target that itself conforms to
SerializerwithTarget == Selfselects that implementation implicitly everywhere. - A separate serializer whose
Targetis another type must be selected explicitly everywhere it is needed.
The same selection rules apply to roots, generated fields, optionals, arrays, sets, and dictionaries. Register a separate serializer and select it explicitly where it is used.
When to Use a Custom Serializer
- The target has private or immutable state.
- The target must enforce construction invariants.
- The target needs a specialized compact encoding.
- The target needs custom validation or construction logic.
- An external enum is not exhaustively switchable.
- An external union cannot represent
UnknownCase.
User-Owned Targets
When you own the target, implement Serializer on the target itself and set
Target to the same type:
import Fory
struct AccountID: Serializer, Equatable {
typealias Target = AccountID
let rawValue: UInt64
static var staticTypeId: TypeId {
.ext
}
static func writeData(
_ value: AccountID,
_ context: WriteContext
) throws {
try UInt64.writeData(value.rawValue, context)
}
static func readData(
_ context: ReadContext
) throws -> AccountID {
AccountID(rawValue: try UInt64.readData(context))
}
}
Register and use the type through the ordinary root APIs:
let fory = Fory()
try fory.register(AccountID.self, id: 300)
let input = AccountID(rawValue: 42)
let data = try fory.serialize(input)
let output: AccountID = try fory.deserialize(data)
assert(input == output)
No with: argument is needed because AccountID.Target == AccountID.
One Global Serializer for an External Type
Swift permits an application to make an external type implement Serializer
through a retroactive conformance:
import Foundation
import Fory
extension UUID: @retroactive Serializer {
public typealias Target = UUID
public static var staticTypeId: TypeId {
.ext
}
public static func defaultValue(
_ context: ReadContext
) throws -> UUID {
_ = context
return UUID(
uuidString: "00000000-0000-0000-0000-000000000000"
)!
}
public static func writeData(
_ value: UUID,
_ context: WriteContext
) throws {
try String.writeData(value.uuidString, context)
}
public static func readData(
_ context: ReadContext
) throws -> UUID {
let raw = try String.readData(context)
guard let uuid = UUID(uuidString: raw) else {
throw ForyError.invalidData("invalid UUID string: \(raw)")
}
return uuid
}
}
Register the external type itself:
try fory.register(UUID.self, id: 300)
let input = UUID()
let data = try fory.serialize(input)
let output: UUID = try fory.deserialize(data)
Because UUID.Target == UUID, unannotated generated fields and ordinary
carriers also select this implementation:
@ForyStruct
struct Request {
var requestID: UUID
}
let input = [UUID(), UUID()]
let data = try fory.serialize(input)
let output: [UUID] = try fory.deserialize(data)
A retroactive conformance applies to the entire process. Swift allows only one
Serializer conformance for a given type; @retroactive acknowledges the
compiler warning but does not make competing conformances safe. Use this form
only when the application intentionally chooses the single global
implementation. Public libraries should generally provide a separate
serializer instead.
Separate Serializers
Use a separate serializer when a public library must not claim a process-global conformance or when an application needs multiple or alternative implementations. The target may be external or user-owned:
import Foundation
import Fory
public enum UUIDStringSerializer: Serializer {
public typealias Target = UUID
public static var staticTypeId: TypeId {
.ext
}
public static func defaultValue(
_ context: ReadContext
) throws -> UUID {
_ = context
return UUID(
uuidString: "00000000-0000-0000-0000-000000000000"
)!
}
public static func writeData(
_ value: UUID,
_ context: WriteContext
) throws {
try String.writeData(value.uuidString, context)
}
public static func readData(
_ context: ReadContext
) throws -> UUID {
let raw = try String.readData(context)
guard let uuid = UUID(uuidString: raw) else {
throw ForyError.invalidData("invalid UUID string: \(raw)")
}
return uuid
}
}
Register the separate serializer and select it explicitly at the root:
let fory = Fory()
try fory.register(UUIDStringSerializer.self, id: 300)
let input = UUID()
let data = try fory.serialize(input, with: UUIDStringSerializer.self)
let output = try fory.deserialize(data, with: UUIDStringSerializer.self)
assert(input == output)
Another declaration, such as UUIDBytesSerializer, may target the same type
with a different body. Fory cannot choose between separate serializers
automatically. Select the desired serializer with with: at roots and with the
matching field annotation. Register only one implementation for the target on
a given Fory instance.
The direct Any and AnyObject root conveniences remain dynamic operations.
A registered serializer may be used for a concrete value passed as Any, but
typed roots and fields still require with: for a separate serializer.
Fields and Carriers
A field whose type directly implements Serializer with Target == Self
needs no selector:
@ForyStruct
struct Request {
var accountID: AccountID
}
A separate serializer must be selected explicitly:
@ForyStruct
struct ExternalRequest {
@ForyField(with: UUIDStringSerializer.self)
var requestID: UUID
}
Ordinary carriers containing types that directly implement Serializer need
no selector. This includes intentional retroactive conformances:
let accountIDs = [
AccountID(rawValue: 1),
AccountID(rawValue: 2),
]
let data = try fory.serialize(accountIDs)
let output: [AccountID] = try fory.deserialize(data)
For an element that uses a separate serializer, name it in the carrier annotation:
@ListField(element: .with(UUIDStringSerializer.self))
var requestIDs: [UUID]
At a root, use the matching carrier serializer:
let data = try fory.serialize(
requestIDs,
with: ArraySerializer<UUIDStringSerializer>.self
)
Custom Serializer Rules
A custom serializer must return .ext from staticTypeId. The
.structType, .enumType, and .typedUnion values are reserved for
@ForyStruct, @ForyEnum, and @ForyUnion.
writeData and readData process only the target body. Do not call a root
serialize or deserialize method from either operation.
Defaults
Implement defaultValue(_:) only when the target has a valid value for a null
or missing field.
Input Validation
Reject invalid input with an appropriate ForyError.
Custom Class Serializers
For a cyclic class, override the complete-value read operation and use Fory's
reference APIs so repeated references resolve to the same object.