title: Custom Serializers sidebar_position: 11 id: custom-serializers license: | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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:
Serializer with Target == Self selects that implementation implicitly everywhere.Target is 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.
UnknownCase.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.
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.
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.
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 )
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.
Implement defaultValue(_:) only when the target has a valid value for a null or missing field.
Reject invalid input with an appropriate ForyError.
For a cyclic class, override the complete-value read operation and use Fory's reference APIs so repeated references resolve to the same object.