Encoding and decoding subclasses

codable

(Vinicius Vendramini) #1

Hi all,

I have a program with a class hierarchy similar to this (only much larger, more nested and more complicated)

class A {
    let b: [B]
}

class B {
}

class B1: B {
    / / ....
}

class B2: B {
    / / ....
}

Class A essentially has an array of instances of B1 and B2 mixed together. If I want to decode class A, I can't just tell it to decode the array as [B].self, as that will call B.init(from: Decoder) which doesn't know how to properly instantiate the B1 and B2 instances.

Right now I'm thinking of a way to wrap these instances and include the class names during the encoding process, so that I can switch over the names later and manually decide which class I want to instantiate. However, as I said, the real hierarchy is considerably more complicated, and so doing this in all instances would be a real chore.

So my question is this: is there any simple way of encoding and decoding this structure that I'm not thinking of? Or does anyone have a better idea?


(Vinicius Vendramini) #2

Quick update: my idea of wrapping the classes didn't work. I also tried encoding the classes into dictionaries I could deal with manually, but it seems [String: Codable] doesn't actually conform to Codable, so I'm at a loss here. So, yeah, any help would be appreciated.


(Amir Abbas Mousavian) #3

I think you need factory initializers. Current Swift, unlike Objective-C does not support factory initializers but there is a workaround. There is a limitation however, you can only use factory initializer in initializers declared as convenience, which means you can't override it in subclasses. Your code would be something like this:

protocol _Factory {
    init(factory: () -> Self)
}

extension Factory {
    init(factory: () -> Self) {
        self = factory()
    }
}

class A {
    let b: [B]
}

class B: Factory {
    convenience init(from decoder: Decoder) throws {
        // Determination logic
        if condition {
            self.init(factory: { return B1(from: decoder) })
        } else {
            self.init(factory: { return B2(from: decoder) })
        }
    }
}

I learned this from swift-corelibs-foundation's NSValue.


#4

Why can't you make a custom Codable type with Codable key and value properties, and archive an array of those rather than the array of Bs?

You will have to implement Decodable manually for this custom type, but it's maybe 5 lines of code.


#5

Depending on your use case, you could use an enum to wrap it.

class A: Codable {
    let bValues: [BWrapper]
    init(bValues: [BWrapper]) {
        self.bValues = bValues
    }
}

class B: Codable {
    let name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "B")
    }
}

class B1: B {
    var otherProperty: String
    init() {
        self.otherProperty = "Other"
        super.init(name: "B1")
    }
    
    required init(from decoder: Decoder) throws {
        // We only need to manually deal with the subclass's properties
        self.otherProperty = try decoder.container(keyedBy: CodingKeys.self)
            .decode(String.self, forKey: .otherProperty)
       try super.init(from: decoder)        
    }
    
    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = try encoder.container(keyedBy: CodingKeys.self)
        // We only need to manually deal with the subclass's properties
        try container.encode(otherProperty, forKey: .otherProperty)
    }
    
    enum CodingKeys: String, CodingKey {
        case otherProperty
    }
}

class B2: B {
    init() {
        super.init(name: "B2")
    }
    
    required init(from decoder: Decoder) throws {
        //If this class has additional properties, you will probably need to handle that here
        try super.init(from: decoder)
    }
}

enum BWrapper: Codable {
    
    enum ErrorTypes: LocalizedError {
        case invalidType
        
        var localizedDescription: String {
            switch self {
            case .invalidType:
                return "Impropper Type of b passed when attemption to decode"
            }
        }
    }
    
    case plainType(B)
    case firstType(B1)
    case secondType(B2)
    
    var caseKey: String {
        switch self {
        case .plainType(_): return "plainType"
        case .firstType(_): return "firstType"
        case .secondType(_): return "secondType"
        }
    }
    
    var bValue: B {
        switch self {
        case .plainType(let b): return b
        case .firstType(let b): return b
        case .secondType(let b): return b
        }
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let caseKey = try values.decode(String.self, forKey: .caseKey)
        switch caseKey {
        case "plainType":
            let value = try values.decode(B.self, forKey: .value)
            self = .plainType(value)
        case "firstType":
            let value = try values.decode(B1.self, forKey: .value)
            self = .firstType(value)
        case "secondType":
            let value = try values.decode(B2.self, forKey: .value)
            self = .secondType(value)
        default:
            throw ErrorTypes.invalidType
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(caseKey, forKey: .caseKey)
        
        switch self {
        case .plainType(let b):
            try container.encode(b, forKey: .value)
        case .firstType(let b):
            try container.encode(b, forKey: .value)
        case .secondType(let b): 
            try container.encode(b, forKey: .value)
        }
    }
    
    enum CodingKeys: String, CodingKey {
        case caseKey
        case value
    }
}

let testItem = A(bValues: [.plainType(B()), .firstType(B1()), .secondType(B2())])

let jsonData = try! JSONEncoder().encode(testItem)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString ?? "" ) 
/*
prints:
{"bValues":[{"caseKey":"plainType","value":{"name":"B"}},{"caseKey":"firstType","value":{"name":"B1","otherProperty":"Other"}},{"caseKey":"secondType","value":{"name":"B2"}}]}
*/
let convertedType = try! JSONDecoder().decode(A.self, from: jsonData)
convertedType.bValues.forEach{ print($0.bValue.name) }
/*
 prints:
 B
 B1
 B2
 */

Admittedly it's a bit messy, but could probably be cleaned up a bit. I'm guessing there's a protocol that could be abstracted from the enum to handle some of the functionality.


(Vinicius Vendramini) #6

Thanks for all your answers! They really helped me get there.

In the end I did something similar to @GetSwifty's suggestion, except my BWrapper was a class instead of an enum. I think there are probably advantages to using an enum, but this project demanded a class and everything turned out fine :slight_smile: