Hi Jes,
you have two options here: Either you use NSCoding
, which supports Polymorphism (i.e. encoding and decoding of subclasses using type information of superclasses) out of the box or you have to extend Codable
yourself. While not trivial, you can extend Codable
to do this, but Codable
also does not support encoding decoding of multiple references to the same instance, no cyclic references and so on. I found myself in the situation that I needed Polymorphism support and multiple reference encoding, so I switched back to using NSCoding
, which supports those things out of the box.
Here are my hints for either way:
Using NSCoding
:
You will find plenty information on that online, essentially you subclass you custom class from NSObject
and make it conform to NSCoding
by implementing encode(with:)
and init?(coder:)
. If you are familiar with custom Codable
implementations, this should be very familiar to you.
Using Codable
:
This gets a little bit tricky and here is my code for supporting Polymorphism (no support for multiple reference encoding). What it does is it essentially uses an enum
to encode type information (which Codable
does not do on its own) with your object when encoding and uses this type information to correctly decode the subclass in the decoding step. This involves manually adding all your custom types (which you want to support Polymorphism) to the PolymorphicType
enum (replace BaseClass
, ChildClass
and ChildChildClass
below with your types). Also any of your custom types, which should support Polymorphic encoding/decoding need to adopt the Polymorphic protocol. If that has been done the code below has all the necessary boiler-plate to do all the rest of the work for you. The only thing you have to do is use polymorphicEncode
and polymorphicDecode
instead of enocode
and deocde
. Those functions are supported for PropertyListEncoder
and any KeyedEncodingContainer
, but you can just use the same extension in the code if you want to extend JSONEncoder
. Same for decoding. There is essentially quite some boilerplate to support Array
s of your custom types and also Optional
s, too, so you could encode [CustomType].self
and [CustomType]?.self
and so forth. If you have any questions regarding this code let me know. I might upload it to GitHub one day with example code, but have not done so yet.
enum PolymorphicType: String, CodingKey {
case base
case child
case childChild
var type: any Polymorphic.Type {
switch self {
case .base:
return BaseClass.self
case .child:
return ChildClass.self
case .childChild:
return ChildChildClass.self
}
}
}
/// objects complying to this protocol are Codable and can be used with polymorphic Codable extension
protocol Polymorphic: Codable {
associatedtype Converted
static var wrappedType: Self.Converted.Type { get }
// static var optionalWrappedType: Self.Converted?.Type { get } // FIXME: testing -> remove
static var polymorphicType: PolymorphicType { get }
}
extension Polymorphic {
typealias Converted = PolymorphicWrapper
static var wrappedType: Self.Converted.Type {
return Self.Converted.self
}
}
protocol OptionalProtocol {
var isSome: Bool { get }
func unwrap() -> Any?
}
extension Optional: Polymorphic where Wrapped: Polymorphic {
typealias Converted = Wrapped.Converted?
static var polymorphicType: PolymorphicType {
return .base
}
}
extension Optional: OptionalProtocol {
var isSome: Bool {
switch self {
case .none:
return false
case .some:
return true
}
}
func unwrap() -> Any? {
switch self {
case .none:
return nil
case .some(let unwrapped):
return unwrapped
}
}
}
// array extension to make nested arrays of inner type Polymorphic also Polymorphic; also enforces static var polymorphicType for encoding purposes as Metatypes cannot be encoded by Codable
extension Array: Polymorphic where Element: Polymorphic {
typealias Converted = [Element.Converted]
static var polymorphicType: PolymorphicType {
return .base
}
}
struct PolymorphicWrapper: Codable {
private enum CodingKeys: String, CodingKey {
case value
case type
}
var reference: any Polymorphic
// MARK: initialization
init(_ reference: any Polymorphic) {
self.reference = reference
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let polymorphicType = PolymorphicType(rawValue: try container.decode(String.self, forKey: .type))!
self.reference = try container.decode(polymorphicType.type, forKey: .value)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.reference, forKey: .value)
try container.encode(type(of: self.reference).polymorphicType.rawValue, forKey: .type)
}
// MARK: static interface
static func recursivePolymorphicWrap<T: Polymorphic>(_ input: T) -> T.Converted {
if let array = input as? [any Polymorphic] {
return array.map { recursivePolymorphicWrap($0) } as! T.Converted
} else if let optional = input as? OptionalProtocol {
if let unwrapped = optional.unwrap() as? any Polymorphic {
return Optional(recursivePolymorphicWrap(unwrapped)) as! T.Converted
} else {
return Optional<any Polymorphic>.none as! T.Converted
}
} else {
return PolymorphicWrapper(input) as! T.Converted
}
}
static func recursivePolymorphicUnwrap(_ input: Any) -> Any {
if let array = input as? [Any] {
return array.map { recursivePolymorphicUnwrap($0) }
} else if let wrapper = input as? PolymorphicWrapper {
return wrapper.reference
} else {
return input
}
}
}
extension PropertyListEncoder {
func encodePolymorphic(_ value: any Polymorphic) throws -> Data {
let wrappedValue = PolymorphicWrapper.recursivePolymorphicWrap(value) as! Encodable
return try self.encode(wrappedValue)
}
}
extension PropertyListDecoder {
func decodePolymorphic<T: Polymorphic>(_ inputType: T.Type, from data: Data, format: inout PropertyListSerialization.PropertyListFormat) throws -> T {
let transformedType = inputType.wrappedType as! Decodable.Type
let wrappedValue = try self.decode(transformedType, from: data, format: &format)
return PolymorphicWrapper.recursivePolymorphicUnwrap(wrappedValue) as! T
}
}
extension KeyedEncodingContainer {
mutating func encodePolymorphic(_ value: any Polymorphic, forKey key: KeyedEncodingContainer.Key) throws {
let wrappedValue = PolymorphicWrapper.recursivePolymorphicWrap(value) as! Encodable
try self.encode(wrappedValue, forKey: key)
}
}
extension KeyedDecodingContainer {
func decodePolymorphic<T: Polymorphic>(_ inputType: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T {
let transformedType = inputType.wrappedType as! Decodable.Type
let wrappedValue = try self.decode(transformedType, forKey: key)
return PolymorphicWrapper.recursivePolymorphicUnwrap(wrappedValue) as! T
}
}