Как декодировать массив унаследованных классов в Swift

#json #swift #encodable

#json #swift #кодируемый

Вопрос:

Проблема: декодировать массив объектов, принадлежащих родительскому и дочернему классам.

Я много читал на эту тему, но мне не удалось найти простого решения.

Я закодировал свойство type, которое предоставляет информацию об исходном классе, но я не нашел способа использовать его при декодировании объекта.

 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)
    }
}

  

Это тестовый код.

 
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)
 }
  

Вывод исходного массива:

 [[p], [c - 2]]
  

Результат декодирования массива:

 [[p], [c]]
  

Как мне изменить родительскую и / или дочернюю функцию инициализации, чтобы правильно декодировать объект?

Очевидно, что мой фактический сценарий намного сложнее. Я должен кодировать / декодировать класс, который содержит массив классов с наследованием. Я пытался использовать это:

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

По-видимому, он отлично работает с массивом Parent, Child, но это не так, если массив находится внутри другого класса.

Более того, я хотел бы узнать решение для повторного использования в других случаях и избежать включения внешней библиотеки, которая строго не требуется.

Комментарии:

1. Я нигде не вижу в этом коде, чтобы вы устанавливали age переменную дочернего элемента. После вызова super.init(name: name) , я думаю, вам следует вызвать self.init(name: name, age: age) . You should also drop the аргумент name`, в self.init котором является избыточным, поскольку он уже установлен в инициализаторе суперкласса.

2. Добавление self.init(имя: имя, возраст: возраст) после super.init(имя: имя) Я получаю эту ошибку, инициализатор не может одновременно делегировать (‘self.init’) и привязывать к инициализатору суперкласса (‘super.init’). 2)

3. Правильно, тогда вызывайте just self.init(name: name, age: age) , поскольку у вас есть super call внутри этого удобного инициализатора.

Ответ №1:

Я думаю, что основная часть проблемы заключается в том, что вы используете массив смешанных типов [Any], а затем вы декодируете его как родительский тип, потому что вполне возможно, чтобы дочерние объекты были правильно закодированы как дочерние.

Одним из решений является создание новой кодируемой структуры, которая содержит массив и которая с использованием свойства type отслеживает, как декодировать объекты в массиве

 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
    }
}
  

Я также внес некоторые изменения в классы, родительский класс значительно упрощен, а дочерний класс имеет измененную функциональность для кодирования / декодирования, где основное изменение заключается в том, что init(from:) вызывает суперы init(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)
    }
}
  

Комментарии:

1. Спасибо за ваш ответ. К сожалению, в моем реальном коде у меня около 30 разных дочерних классов, поэтому разделение массива по типу класса не является вариантом.

2. @Fab Я опубликовал новое решение

3. @Fab Почему бы вам не превратить свой родительский класс в протокол и не заставить дочерние классы реализовать его, это был бы правильный способ сделать это, и вы бы обошли проблему с декодированием.

4. Я также работал над аналогичным решением, оно мне не очень нравится, но я думаю, что это единственный доступный вариант. Однако, чтобы заставить его работать, мне пришлось добавить *** try container.encode(ObjectType.parent, forKey: .type) *** и *** try container.encode(ObjectType.child, forKey: .type) *** в функции encode соответствующих классов. Ответ принят, спасибо.