Search code examples
swiftinheritancecodabledecodableencodable

Swift Attempt to a decode a Codable parent class as one of its subclasses


Say I have parent class A: Codable with subclasses B1: A and B2: A. A different class Main: Codable in my application has a pointer to an A which can either be a B1 or a B2 but cannot be an A (I'm effectively treating A as an abstract class).

When I am decoding a Main, I run into an issue where it is incorrectly being decoded to an abstract A rather than a B1 or B2, even though the value store in A will always be a B1 or a B2. I have tried implementing custom init(from decoder: Decoder) and func encode(to encoder: Encoder)s in the subclasses but when I step through Main's decode logic in my running app, I never see those subclasses' implementations being called.

Is this because Main has an A and has no idea to even attempt to decode it as a B1 or a B2? Do I need to call those subclasses decoders specifically? If the latter is that case, those subclasses decoders couldn't call the parent decoder because that would create an infinite loop.

Here is what my code currently looks like:

class Main: Codable {
    let a: A
}

class A: Codable {

}

class B1: A {
    let b1Only: Int

    private enum CodingKeys: String, CodingKey {
        case b1Only
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        b1Only = try container.decode(Int.self, forKey: .b1Only)
        try super.init(from: decoder)
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.b1Only, forKey: .b1Only)
        try super.encode(to: encoder)
    }
}

class B2: A {
    let b2Only: Int

    private enum CodingKeys: String, CodingKey {
        case b2Only
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        b2Only = try container.decode(Int.self, forKey: .b2Only)
        try super.init(from: decoder)
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.b2Only, forKey: .b2Only)
        try super.encode(to: encoder)
    }
}

Solution

  • You need to have a custom init(from:) in Main and decode a to the right subclass directly

    class Main: Codable {
        let a: A
    
        required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            if let object = try? container.decode(B1.self, forKey: .a) {
                a = object
            } else {
                a = try container.decode(B2.self, forKey: .a)
            }
        }
    }