0

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)
    }
}
Evan Kaminsky
  • 695
  • 10
  • 23

1 Answers1

1

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)
        }
    }
}
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • Thank you, this works. Is there any alternate solution where A can determine if it is a B1 or B2 without an outside object like Main, maybe in A's own init(from decoder)? If not, this is fine, it just needs to be repeated for each object that has an A. – Evan Kaminsky Mar 20 '22 at 22:32
  • 1
    The problem is that if you want to decode as A.self then the result can only be an instance of A. If you somehow tries to to initialise a B1 from A then the init(from:) for B1 must call super.init(from:) and you'll get an infinite recursion – Joakim Danielson Mar 21 '22 at 07:18