Coding Adapter provides you with type specific serialization, so that you can decode specific subclasses where they are hidden behind some common ancestor or hide codable objects behind a protocol typed variable.
This also works with Swift on linux (tested).
Add this line to your Package.swift
dependencies
.package(url: "https://github.com/IgorMuzyka/Type-Preserving-Coding-Adapter", from: "1.0.0"),
pod 'TypePreservingCodingAdapter', :git => 'https://github.com/IgorMuzyka/Type-Preserving-Coding-Adapter.git'
Lets say you want to define Animal Protocol. (This would also work if Animal was a Class and Dog and Cat would inherit from it).
public protocol Animal: Codable {
func say() -> String
}
And then you have a Dog and a Cat structs which conform to it.
public struct Dog: Animal, Codable {
public func say() -> String { return "bark" }
}
public struct Cat: Animal, Codable {
public func say() -> String { return "meow" }
}
And then you have some Zoo struct which holds your animals as Animal and is codable. You decide if the animals gets encoded by Alias or Signature by defining Strategy when wrapping animals in encode function below.
public struct Zoo: Codable {
private enum CodingKeys: CodingKey {
case animals
}
public let animals: [Animal]
public init(animals: [Animal]) {
self.animals = animals
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.animals = try container.decode([Wrap].self, forKey: .animals).map { $0.wrapped as! Animal }
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(animals.map { Wrap(wrapped: $0, strategy: .alias) }, forKey: .animals)
}
}
To preserve types while decoding and encoding the Zoo you'll need to have some setup close to this.
Create an Adapter. Set it to userInfo of Encoder and Decoder you would use. And register your types and their aliases in Adapter.
let adapter = TypePreservingCodingAdapter()
let encoder = JSONEncoder()
let decoder = JSONDecoder()
encoder.userInfo[.typePreservingAdapter] = adapter
decoder.userInfo[.typePreservingAdapter] = adapter
adapter
.register(type: Cat.self)
.register(alias: "cat", for: Cat.self)
.register(type: Dog.self)
.register(alias: "dog", for: Dog.self)
let zoo = Zoo(animals: [Cat(), Dog(), Cat(), Dog()])
let data = try! encoder.encode(zoo)
let decodedZoo = try! decoder.decode(Zoo.self, from: data)
After decoding Cat and Dog instances would be of correct type. (behaves the same if Animal, Dog and Cat would be classes).
- Signature - is a string made from the Type itself so it should be consistent throughout the target (target name is a frontmost prefix part of a signature). Will differ between targets. Signature for types may also differ between some versions of iOS SDK. Example:
__ObjC.UIView.Type
and__C.UIView.Type
. That's when you may need alias. - Alias - is a way to enable coding when communicating between multiple platforms but still using Swift. For example when you have different targets for iOS, macOS and Linux the target name will be a prefix of your type Signature so you'll want to use an alias to support the same struct or a class coding between your targets.
- TypePreservingCodingAdapter - is an adapter inserted into Encoder and/or Decoder to help hide your real classes or structs behind a base class or a protocol (as shown in example above in a Protocol usage example). You'll want to register your types and aliases for them with Adapter by using
adapter.register(type: MyClass.self)
oradapter.register(alias: "my_class", for: MyClass.self)
. - Wrap - is a helper which allows seamless usage of a TypePreservingCodingAdapter with your types. Also Wrap allows you to select a coding strategy which can be
.signature
or.alias
. And as show in example you can use Wrap to wrap your types and be sure their types are not missed when you decoding.
Igor Muzyka, igormuzyka42@gmail.com
TypePreservingCodingAdapter is available under the MIT license. See the LICENSE file for more info.