Comment décoder un tableau de classes héritées dans Swift

Oct 03 2020

Le problème: décoder un tableau d'objets appartenant aux classes Parent et Child.

J'ai lu beaucoup de trucs à ce sujet mais je n'ai pas pu trouver de solution simple.

J'ai encodé une propriété de type qui fournit les informations de la classe d'origine, mais je n'ai pas trouvé de moyen de l'utiliser pour décoder l'objet.

class Parent: Codable, CustomDebugStringConvertible {
    var debugDescription: String {
        return "[\(name)]"
    }
    
    var name: String
    init(name: String) {
        self.name = name
    }
    
    enum CodingKeys: CodingKey {
        case name
        case type
        case age
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try! container.decode(String.self, forKey: .name)
        let type = try! container.decode(String.self, forKey: .type)
        print("Reading \(type)")
        
        if type == "Child" {
            try Child.init(from: decoder)
            return
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode("Parent", forKey: .type)
        try container.encode(name, forKey: .name)
    }
}


class Child: Parent {
    
    override var debugDescription: String {
        return "[\(name) - \(age)]"
    }
    var age: Int

    init(name: String, age: Int) {
        self.age = age
        super.init(name: name)
    }
    
    enum CodingKeys: CodingKey {
        case name
        case age
        case type
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        age = try! container.decode(Int.self, forKey: .age)
        let name = try! container.decode(String.self, forKey: .name)
        super.init(name: name) // I think the problem is here!
    }
    
    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode("Child", forKey: .type)
        try container.encode(age, forKey: .age)
    }
}

Ceci est le code de test.


let array = [Parent(name: "p"), Child(name: "c",age: 2)]
print(array)
        
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let decoder = JSONDecoder()
do {
   let jsonData = try encoder.encode(array)
   let s = String(data: jsonData, encoding: .ascii)
   print("Json Data \(s!)")
            
    let decodedArray = try decoder.decode([Parent].self, from: jsonData)
            
    print(decodedArray)
 }
 catch {
    print(error.localizedDescription)
 }

La sortie du tableau d'origine est:

[[p], [c - 2]]

La sortie du tableau de décodage est:

[[p], [c]]

Comment changer la fonction Init Parent et / ou Enfant afin de décoder correctement l'objet?

De toute évidence, mon scénario actuel est beaucoup plus complexe. Je dois encoder / décoder une classe qui contient un tableau de classes avec héritage. J'ai essayé d'utiliser ceci:

https://github.com/IgorMuzyka/Type-Preserving-Coding-Adapter

Apparemment, cela fonctionne bien sur un tableau de Parent, Child, mais ce n'est pas le cas si le tableau est dans une autre classe.

De plus, j'aimerais apprendre une solution pour réutiliser dans d'autres cas et éviter d'inclure une bibliothèque externe n'est pas strictement nécessaire.

Réponses

2 JoakimDanielson Oct 03 2020 at 09:52

Je pense qu'une partie importante du problème est que vous utilisez un tableau de types mixtes, [Any], et ensuite vous le décodez comme un type Parent car il est tout à fait possible de faire encoder correctement les objets enfants en Child.

Une solution consiste à créer une nouvelle structure Codable qui contient le tableau et qui, avec l'utilisation d'une propriété de type, assure le suivi de la façon de décoder les objets du tableau

enum ObjectType: String, Codable {
    case parent
    case child
}

struct ParentAndChild: Codable {
    let objects: [Parent]

    enum CodingKeys: CodingKey {
        case objects
    }

    enum ObjectTypeKey: CodingKey {
        case type
    }

    init(with objects: [Parent]) {
        self.objects = objects
    }

    init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            var objectsArray = try container.nestedUnkeyedContainer(forKey: CodingKeys.objects)
            var items = [Parent]()

        var array = objectsArray
        while !objectsArray.isAtEnd {
            let object = try objectsArray.nestedContainer(keyedBy: ObjectTypeKey.self)
            let type = try object.decode(ObjectType.self, forKey: ObjectTypeKey.type)
            switch type {
            case .parent:
                items.append(try array.decode(Parent.self))
            case .child:
                items.append(try array.decode(Child.self))
            }
        }
        self.objects = items
    }
}

J'ai également apporté des modifications aux classes, la classe Parent est extrêmement simplifiée et la classe Child a modifié des fonctionnalités pour l'encodage / décodage où le changement principal est d' init(from:)appeler supersinit(from:)

class Parent: Codable, CustomDebugStringConvertible {
    var debugDescription: String {
        return "[\(name)]"
    }

    var name: String
    init(name: String) {
        self.name = name
    }
}

class Child: Parent {

    override var debugDescription: String {
        return "[\(name) - \(age)]"
    }
    var age: Int

    init(name: String, age: Int) {
        self.age = age
        super.init(name: name)
    }

    enum CodingKeys: CodingKey {
        case age
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        age = try container.decode(Int.self, forKey: .age)
        try super.init(from: decoder)
    }

    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(age, forKey: .age)
    }
}