How can I decode when I don't know the type, with class inheritance? - swift

I have a base class Action, which is an Operation. It has a bunch of crufty Operation stuff in it (KVO and all that). The base class itself doesn't actually need to encode/decode anything.
class Action : Operation, Codable {
var _executing = false
...
}
I have a bunch of Action sub-classes, like DropboxUploadAction, which are directly instantiated with an Input struct they define:
let actionInput = DropboxUploadAction.Input.init(...)
ActionManager.shared.run(DropboxUploadAction.init(actionInput, data: binaryData), completionBlock: nil)
Here's what the subclasses look like:
class DropboxUploadAction : Action {
struct Input : Codable {
var guid: String
var eventName: String
var fileURL: URL?
var filenameOnDropbox: String
var share: Bool
}
struct Output : Codable {
var sharedFileLink: String?
var dropboxPath: String?
}
var input: Input
var output: Output
...
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
input = try values.decode(Input.self, forKey: .input)
output = try values.decode(Output.self, forKey: .output)
let superDecoder = try values.superDecoder()
try super.init(from: superDecoder)
}
fileprivate enum CodingKeys: String, CodingKey {
case input
case output
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(input, forKey: .input)
try container.encode(output, forKey: .output)
try super.encode(to: container.superEncoder())
}
}
When some situations occur such as a loss of internet connectivity, these classes need to be serialized to disk for later. That's fine, because at the time I have references to them and can encode them with JSONEncoder().encode(action), no problem.
But later when I want to deserialize them, I need to specify the type of the class and I don't know what it is. I have some data and I know it can be decoded to a class that inherits from Action, but I don't know which subclass it is. I'm loathe to encode that in the filename. Is there some way to decode it as the base class Action, then in the decode() method of Action, somehow detect the proper class and redirect?
In the past I've used NSKeyedUnarchiver.setClass() to handle this. But I don't know how to do that with Swift 4's Codable, and I understand that NSCoding is deprecated now so I shouldn't use NSKeyedUnarchiver anymore...
If it helps: I have a struct Types : OptionSet, Codable which each subclass returns, so I don't have to use the name of the class as its identity.
Thanks for any help!

Uhhh NSCoding isn't deprecated. We still use it when instantiating UIViewControllers from storyboard via init(coder:).
Also, if you still don't want to use NSCoding, you can just store the Input, Output and Types to a struct and serialize that to disk instead.
struct SerializedAction {
let input: Input
let output: Output
let type: Type
}
When needed, you can decode that and decide the correct Action to initialize with your input/output via the type property.
class DropboxAction: Action {
...
init(input: Input, output: Output) {
...
}
}
You don't necessarily need to encode the entire Action object.

Related

How do I (simply) "do something else" to the resulting struct after the JSONDecoder().decode does it's job?

Very simply, I have this:
public struct SomeAPI: Codable {
public let a: String
public let b: String
public let c: String
public let d: String
}
and I of course do this ...
let s = try? JSONDecoder().decode(SomeAPI.self, from: data)
Imagine I'm decoding a few hundred of those, perhaps in an array.
Very simply, after each one is decoded, I want to run some code.
So conceptually something like ..
public struct SomeAPI: Codable {
init-something() {
super.something = magic
print("I just decoded one of the thingies! a is \(a)")
}
public let a: String
public let b: String
public let c: String
public let d: String
}
I'm afraid I have absolutely no idea how to do that, having searched much. How is it done? It seems a shame to do it manually as a separate step.
The Encoding and Decoding Custom Types documentation is a great starting point for understanding how to implement the Encodable and Decodable method requirements to add to your types — and specifically, the Encoding and Decoding Manually section shows how to override the compiler-synthesized implementation of these methods to provide your own.
It's important to understand how the automated approach to Codable works, though. In a nutshell: for a type that adopts Encodable/Decodable, the compiler checks whether it's provided its own implementation of the associated method, and if not, attempts to fill it in on its own. There's no magic there: it fills in its internal state (manipulating the AST) with an implementation as if you had written it yourself in code.
For your example SomeAPI type, the generated code inside of the compiler looks exactly as if you had typed the following in yourself:
struct SomeAPI: Codable {
let a: String
let b: String
let c: String
let d: String
private enum CodingKeys: String, CodingKey {
case a, b, c, d
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
a = try container.decode(String.self, forKey: .a)
b = try container.decode(String.self, forKey: .b)
c = try container.decode(String.self, forKey: .c)
d = try container.decode(String.self, forKey: .d)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(a, forKey: .a)
try container.encode(b, forKey: .b)
try container.encode(c, forKey: .c)
try container.encode(d, forKey: .d)
}
}
This only happens if you haven't implemented the protocol requirement yourself, though: if you have, the compiler leaves the existing implementation alone, assuming you've done exactly what you want.
This last point is relevant to your use-case, because it sounds like what you'd like to do is implement this type like
struct SomeAPI: Codable {
let a: String
let b: String
let c: String
let d: String
init(from decoder: Decoder) throws {
somehowCallTheCompilerSynthesizedImplementationOfThisMethod()
/* perform some other stuff */
}
}
The issue is that when you provide your own init(from:) implementation, the compiler won't synthesize anything for your type, so there's no "default" implementation to call; unfortunately, it's an all-or-nothing deal.
To do this, then, you can either:
Implement init(from:) on your types by implementing the full method, then adding whatever code you'd like, or
Inherit the compiler-synthesized implementation in a bit of a roundabout way: if you turn SomeAPI into a class, you can allow it to receive the default synthesized implementation, then override the synthesized implementation to do what you want (demonstrated here by moving its contents into a "dummy" parent type, then keep using SomeAPI as a subclass):
class _SomeAPI: Codable {
let a: String
let b: String
let c: String
let d: String
}
class SomeAPI: _SomeAPI {
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
/* do whatever */
}
}
The downside of this approach, of course, is that this changes the semantics of the SomeAPI type, which may very well not be what you want
Approach (1) is likely a good starting approach as it keeps the semantics of this type the same, and there are tools out there that can help automatically output code for a given Swift type (e.g. Sourcery) if this gets repetitive.

Decoding objects without knowing their type first

There is a likelihood this is an XY problem, I am open to these suggestions as well !
I am trying to work with Minecraft save data. Minecraft encodes Entities (basically anything that is not strictly a block) with their type inside an id property . The file then contains a big array of entities, which I want to decode and instantiate.
The problem is that, using Decodable, I must know an object's type before I start instantiating it like container.decode(Zombie.self). I can't figure out how to create a function that would read the id and return the right type of entity ?
I think this explains what I need better than any explanation could :
//Entity objects don't actually store their ID since re-encoding it is trivial.
protocol Entity : Decodable {var someProperty : Int {get set}}
struct Zombie : Entity {var someProperty : Int}
struct Skeleton : Entity {var someProperty : Int}
//Using JSON instead of SNBT so we can use JSONDecoder
let jsonData = """
[
{
"id":"zombie",
"someProperty":"3"
},
{
"id" : "skeleton",
"someProperty":"3"
}
]
"""
struct EntityList : Decodable {
var list : [any Entity] = []
init(from decoder : Decoder) throws {
var container = try decoder.unkeyedContainer()
//What should we put here ?
}
}
let decoder = JSONDecoder()
let entityList = try decoder.decode(EntityList.self, from: Data(jsonData.utf8))
//entityList should be [Zombie, Skeleton]
At the moment I'm looking into the Factory pattern, maybe that's an interesting lead ? In any case, thank you for your help !
( Please note this question has nothing to do with decoding the actual binary contents of the file, it was honestly quite hard to do but I already have a working Encoder / Decoder. It is only about unpacking those contents, hence why I just used JSON in the example above, since we have a common Decoder for that. )
I honestly haven't used the new any syntax enough to know if that can help but I have done what you're trying to do numerous times and here is how I do it.
Set up the data first
We first declare what a Zombie and a Skeleton are. They could just inherit from a protocol or they could be separate structs...
struct Zombie: Decodable {
let someProperty: Int
}
struct Skeleton: Decodable {
let someProperty: Int
let skeletonSpecificProperty: String
}
Then we can turn your array of [anyEntityType] into a homogeneous array by using an enum and embedding the entities into it...
enum Entity: Decodable {
case zombie(Zombie)
case skeleton(Skeleton)
}
Decode the enum given your JSON structure
We have to provide a custom decoder for the Entity type...
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)
// First get the `id` value from the JSON object
let type = try container.decode(String.self, forKey: .id)
// check the value for each type of entity we can decode
switch type {
// for each value of `id` create the related type
case "zombie":
let zombie = try Zombie(from: decoder)
self = .zombie(zombie)
case "skeleton":
let skeleton = try Skeleton(from: decoder)
self = .skeleton(skeleton)
default:
// throw an error here... unsupported type or something
}
}
This should now let you decode an array of Entities from JSON into an [Entity] array.
Deal with "unknown" types
There is an extra step required for dealing with the "unknown" types. For instance, in the code above. If the JSON contains "id": "creeper" this will error as it can't deal with that. And you'll end up with your whole array failing to decode.
I've created a couple of helper functions that help with that...
If you create an object like...
struct Minecraft: Decodable {
let entities: [Entity]
enum RootKeys: String, CodingKey {
case entities
}
}
And these helpers...
extension KeyedDecodingContainer {
func decodeAny<T: Decodable>(_ type: T.Type, forKey key: K) throws -> [T] {
var items = try nestedUnkeyedContainer(forKey: key)
var itemsArray: [T] = []
while !items.isAtEnd {
guard let item = try? items.decode(T.self) else {
try items.skip()
continue
}
itemsArray.append(item)
}
return itemsArray
}
}
private struct Empty: Decodable { }
extension UnkeyedDecodingContainer {
mutating func skip() throws {
_ = try decode(Empty.self)
}
}
You can create a custom decoder for the Minecraft type like this...
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)
self.entities = try container.decodeAny(Entity.self, forKey: .entities)
}

Decoding/Encoding a struct with protocol type properties

I am trying to save a configuration data structure with UserDefaults, thus the data structure needs to conform to the Codable protocol. This is my data structure:
// Data structure which saves two objects, which conform to the Connection protocol
struct Configuration {
var from: Connection
var to: Connection
}
protocol Connection: Codable {
var path: String { get set }
}
// Two implementations of the Connection protocol
struct SFTPConnection: Connection, Codable {
var path: String
var user: String
var sshKey: String
}
struct FTPConnection: Connection, Codable {
var path: String
var user: String
var password: String
}
If I just add Codable to Configuration, it won't work. So I have to implement it myself.
extension Configuration: Codable {
enum CodingKeys: String, CodingKey {
case from, to
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let from = try container.decode(Connection.self, forKey: .from)
let to = try container.decode(Connection.self, forKey: .to)
self.from = from
self.to = to
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(from, forKey: .from)
try container.encode(to, forKey: .to)
}
}
For every call on decode() or encode() I get the error Protocol type 'Connection' cannot conform to 'Decodable/Encodable' because only concrete types can conform to protocols.
I can see that it is difficult for the compiler to identify, which class should be used to decode the given object. But I figured it should be easy to encode an object, since every object of type Connection implements the encode() method.
I know, that the problem lies with the protocol and that the protocol can't be used with Decodable/Encodable. How would I change the code in decode/encode, so that I can still use the protocol with the various implementations? My guess is to somehow tell decode/encode which implementation of the protocol to use. I would appreciate any elegant solutions for this problem!
It's a limitation of Swift that a protocol cannot conform to itself. Thus from and to do not conform to Codable as bizarre as that seems.
You can get around it by using generics which basically means you declare from and to as arbitrary types that conform to Codable. Here's how:
struct Configuration<F: Connection, T: Connection>: Codable {
var from: F
var to: T
}
let myFrom = SFTPConnection(path: "foo", user: "me", sshKey: "hgfnjsfdjs")
let myTo = FTPConnection(path: "foo", user: "me", password: "hgfnjsfdjs")
let example = Configuration(from: myFrom, to: myTo)
So F and T are types that conform to Connection. When you instantiate example in the last line, the compiler infers F is SFTPConnection and T is FTPConnection.
Once I added the generic parameters, Configuration was able to synthesise the conformance to Codable without the extension.
To answer Sh_kahn's point about having two generic parameters, I did this to allow from and to to be connections of different types. If you always want the two connections to be of the same type i.e. always two SFTPConnections or two FTPConnections you should declare the Configuration like this:
struct Configuration<C: Connection>: Codable {
var from: C
var to: C
}

Trying to make a class codable in Swift but it is not the correct format?

I am creating a class that conforms to codable.
I have this:
import Foundation
class Attribute : Decodable {
var number: Int16
var label: String?
var comments: String?
init(number:Int16, label:String?, comments:String?) {
self.number = number
self.label = label
self.comments = comments
}
// Everything from here on is generated for you by the compiler
required init(from decoder: Decoder) throws {
let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
number = try keyedContainer.decode(Int16.self, forKey: .number)
label = try keyedContainer.decode(String.self, forKey: .label)
comments = try keyedContainer.decode(String.self, forKey: .comments)
}
enum CodingKeys: String, CodingKey {
case number
case label
case comments
}
}
extension Attribute: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(number, forKey: .number)
try container.encode(label, forKey: .label)
try container.encode(comments, forKey: .comments)
}
}
This is apparently fine.
I create an instance of Attribute and encode it using:
let newAttribute = Attribute.init(number:value.number, label:value.label, comments:value.shortcut)
Then I create an array of these attributes and encode that array using
let array = try JSONEncoder().encode(array)
This will encode the array of Attribute to Data.
Then I try to convert the Data object back to the array of Attribute using this:
let array = try JSONDecoder().decode(Attribute.self, from: data) as! Array<Attribute>
First error I get is this:
Cast from 'Attribute' to unrelated type 'Array< Attribute>' always fails
If I remove the cast part I catch this error when the decode tries...
Optional("The data isn’t in the correct format.")
Any ideas?
You need to pass in the array to decode, don't pass in the array element type, then try to force-cast that to an array, that doesn't make any sense. YourType and Array<YourType> are two different and completely unrelated types, so you cannot cast one to the other and you need to use the specific type when calling JSONDecoder.decode(_:from:).
let array = try JSONDecoder().decode([Attribute].self, from: data)
Btw as already pointed out in your previous question, there is no need to manually write the init(from:) and encode(to:) methods or the CodingKeys enum since for your simple type, the compiler can auto-synthesise all of those for you. Also, if you used a struct instead of class, you'd also get the member wise initialiser for free.

Decode a Swift type that is a wrapped Codable type with an extra Codable property

I've got a Codable type, let's say Car, that is defined as:
struct Car: Codable {
let age: Int
let color: String
}
I can encode/decode this just fine.
With my persistence system, when an object is stored it gets assigned an _id property, which is a String, e.g. 5cae04b533376609456d40ed.
As such, when I read the Data from the persistent store and then try to decode it there are extra bytes in there that represent the _id property and its associated String value.
I'm not in control of the various types that can be encoded and stored in the store. The only restriction on them is that they are Codable.
What I want to be able to do is decode the Data that I get when reading from the store (with the _id stuff included) into a type that is something like Wrapped<T: Codable>, which would be defined as something like (in the simplest form):
struct Wrapped<T: Codable> {
let _id: String
let value: T
}
However, I'm not sure to go about this.
One attempt I made was to to define a custom decode function but that didn't get very far as I can't seem to access the T type's CodingKeys, which makes things, as far as I can tell, impossible with that approach.
Maybe there's another approach that would make things work as I'd like?
You can write a custom decode function for your Wrapped type that parses out the _id and then passes the decoder along to the wrapped type so it can decode it's own properties:
struct Wrapped<T: Codable>: Decodable {
let _id: String
let value: T
private enum CodingKeys: String, CodingKey {
case _id
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
_id = try container.decode(String.self, forKey: ._id)
value = try T(from: decoder)
}
}
You can simply declare that the _id property shouldn't decoded by defining your custom CodingKeys and omitting _id from there. You also need to assign a default value to the non-decoded properties (_id in your case) if you want to use the automatically synthetised initializer.
For a concrete type:
struct Car: Codable {
let age: Int
let color: String
let _id:Int = 0
enum CodingKeys: String, CodingKey {
case age, color
}
}
You can achieve this for all your persisted types.
If you don't want to create the CodingKeys enum for all persisted types, you could follow the generic wrapper type approach you started, but you'll need to create custom init(from:) and encode(to:) methods.
struct Persisted<T: Codable>: Codable {
let _id:Int = 0
let value:T
init(from decoder:Decoder) throws {
value = try decoder.singleValueContainer().decode(T.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}