0

When I decode an array of the subclass(RegularCard), I get an array of the superclass (Card). The encoding is fine, I tested it and the encoding worked fine. But, when I decode, the function to decode the subclass(RegularCard) isn't called. My code is below. I found the error in another post with the following:

Update June 25 '17: I ended up filing a bug with Apple about this. rdar://32911973 - Unfortunately an encode/decode cycle of an array of Superclass that contains Subclass: Superclass elements will result in all elements in the array being decoded as Superclass (the subclass' init(from:) is never called, resulting in data loss or worse).

This was in this post.

class Card : Codable {

    var id: Int
    var front: String
    var score: Int
    var cardType : CardType
    
    init(id: Int, front : String, score: Int, cardType : CardType) {
        self.id = id
        self.front = front
        self.score = score
        self.cardType = cardType
    }
    
    enum CodingKeys : String, CodingKey {
        case id
        case front
        case score
        case cardType
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        print("encoding super")
        try container.encode(id, forKey: .id)
        try container.encode(front, forKey: .front)
        try container.encode(score, forKey: .score)
        try container.encode(cardType.rawValue, forKey: .cardType)
    }
    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        print("decoding super")
        self.id = try values.decode(Int.self, forKey: .id)
        self.front = try values.decode(String.self, forKey: .front)
        self.score = try values.decode(Int.self, forKey: .score)
        self.cardType = CardType(rawValue: try values.decode(String.self, forKey: .cardType))!
    }
}


enum CardType : String {
    case regular = "regular"
    case multipleChoice = "multipleChoice"
    case numbered = "numbered"
    case bulleted = "bulleted"
    case acronym = "acronym"
    case image = "image"
}

class RegularCard : Card {
    var back: String
    
    init(id: Int, front : String, score: Int, back : String) {
        self.back = back
        super.init(id: id, front: front, score: score, cardType: .regular)
    }
    enum CodingKeys : String, CodingKey {
        case back
        case id
        case front
        case score
        case cardType
    }
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        print("encoding Regular")
        try super.encode(to: encoder)
        try container.encode(back, forKey: .back)
    }
    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        print("decoding regular")
        back = try values.decode(String.self, forKey: .back)
        try super.init(from: decoder)

    }
}

'''

Asperi
  • 228,894
  • 20
  • 464
  • 690
Belal Elsiesy
  • 49
  • 1
  • 1
  • 3
  • please edit your question and post the json – Leo Dabus Sep 20 '20 at 00:04
  • Show how you are decoding your cards. Are you sure you are not decoding an array of cards `JSONDecoder().decode([Card].self, ...` instead of an array of regular cards `JSONDecoder().decode([RegularCard].self, ...`? – Leo Dabus Sep 20 '20 at 00:21

1 Answers1

1

Works fine for me:

    let reg = RegularCard(id: 1, front: "yo", score: 20, back: "frf")
    let arr = [reg]
    let data = try! JSONEncoder().encode(arr)
    let arr2 = try! JSONDecoder().decode([RegularCard].self, from: data)
    print(type(of:arr2)) // Array<RegularCard>
    let what = arr2.first!
    print(what.id, what.front, what.score, what.back) // 1 yo 20 frf

Note that I did have to change your code a little; I changed your second CodingKeys to CodingKeys2, since otherwise I couldn't get your code to compile.

class RegularCard : Card {
    var back: String
    
    init(id: Int, front : String, score: Int, back : String) {
        self.back = back
        super.init(id: id, front: front, score: score, cardType: .regular)
    }
    enum CodingKeys2 : String, CodingKey {
        case back
        case id
        case front
        case score
        case cardType
    }
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys2.self)
        print("encoding Regular")
        try super.encode(to: encoder)
        try container.encode(back, forKey: .back)
    }
    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys2.self)
        print("decoding regular")
        back = try values.decode(String.self, forKey: .back)
        try super.init(from: decoder)
        
    }
}

I do see your point, however; even if RegularCard went in, if we decode as Card, we cannot recover the value as a RegularCard (it can't be downcast). That's just the nature of Codable or generics, I guess. Basically, my advice would be, don't do that.

However, what you can do in this situation is try to decode as a RegularCard, and if that doesn't work, decode as a Card. That way you always get the right answer.

let card = Card(id: 2, front: "hoo", score: 10, cardType: .regular)
let reg = RegularCard(id: 1, front: "yo", score: 20, back: "frf")
let arr1 = [card]
let arr2 = [reg]
let data1 = try! JSONEncoder().encode(arr1)
let data2 = try! JSONEncoder().encode(arr2)

// first data1
do {
    let result1 = try JSONDecoder().decode([RegularCard].self, from: data1)
    print(result1)
} catch {
    let result2 = try JSONDecoder().decode([Card].self, from: data1)
    print(result2) // it's a Card
}

// now data2
do {
    let result1 = try JSONDecoder().decode([RegularCard].self, from: data2)
    print(result1) // it's a RegularCard
} catch {
    let result2 = try JSONDecoder().decode([Card].self, from: data2)
    print(result2)
}
matt
  • 515,959
  • 87
  • 875
  • 1,141