Viren Patel
Viren Patel

Reputation: 128

Codable with inheritance

I'm serializing and deserializing inherited classes using Codable protocol. I'm successfully able to serialize whole model into JSON but I'm having a trouble deserialize the JSON. This my data structure looks like. Some part of my application work like the below example and at this point it will be too much work for us to change the whole data structure.

class Base: Codable {
     let baseValue: String
     init(baseValue :String) {
         self.baseValue = baseValue
     }
     enum SuperCodingKeys: String, CodingKey {
         case baseValue
     }

     func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: SuperCodingKeys.self)
        try container.encode(baseValue, forKey: .baseValue)
     }
}

class Child1: Base {
    let child1Value: Int
    init(child1Value: Int, baseValue: String) {
        self.child1Value = child1Value
        super.init(baseValue: baseValue)
    }

    private enum CodingKeys: String, CodingKey {
        case child1Value
    }

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

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

    }

}

class Child2: Base {
    let child2Value: Int
    init(child2Value: Int, baseValue: String) {
        self.child2Value = child2Value
        super.init(baseValue: baseValue)
    }


    private enum CodingKeys: String, CodingKey {
        case child2Value
    }

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

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

    }
}


class DataManager: Codable {
    var bases: [Base] = []
    init(bases: [Base]) {
        self.bases = bases
    }
}

This how I add value to DataManger(btw in my application this a singleton class which holds the whole data for the app. I'm trying to implement Codable protocol on this DataManager class)

let child1 = Child1(child1Value: 1, baseValue: "Child1Value_Base")
let child2 = Child2(child2Value: 2, baseValue: "Child2Value_Base")

let dataManager = DataManager(bases: [])

dataManager.bases.append(child1 as Base)
dataManager.bases.append(child2 as Base)

I'm using JSONEncoder() to encode the model into data by using this code. It's working fine up to this point.

let dataManagerData = try JSONEncoder().encode(dataManager)
print(String(data: dataManagerData, encoding: .utf8))

This what json looks like after Encoding

{
    "bases":[{
        "child1Value":1,
        "baseValue":"Child1Value_Base"
    },
    {
        "child2Value":2,
        "baseValue":"Child2Value_Base"
    }]
}

So when I try to decode this JSON by using below code I'm only able to decode it up to the Base(parent class) level, not to the child level.

let dataManager = try JSONDecoder().decode(DataManager.self, from: dataManagerData)

And this what I'm able to get out of it.

{
    "bases":[{
        "baseValue":"Child1Value_Base"
    },
    {
        "baseValue":"Child2Value_Base"
    }]
}

To solve this problem I tried to manually decode by using this way but JSONDecoder gives me 0 counts of Bases.

class DataManager: Codable {
    var bases: [Base] = []
    init(bases: [Base]) {
        self.bases = bases
    }
    private enum CodingKeys: String, CodingKey {
        case bases
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            // I think decoder is trying to decode the bases object based on Child1 or Child2 class and it fail. 
            if let value = try? container.decode(Child1.self, forKey: .bases) {
                bases.append(value as Base)
            } else if let value = try? container.decode(Child2.self, forKey: .bases){
                bases.append(value as Base)
            }
        } catch  {
            print(error)
        }
    }
}

So my questions are

  1. how to deserialize this array of bases based on their respective child class and add them to DataManager?
  2. Is there any way in "init(from decoder: Decoder)" method where we can get the value of key and iterate through them one by one for decoding to their respective class.

Upvotes: 2

Views: 2641

Answers (1)

Viren Patel
Viren Patel

Reputation: 128

If anyone is having the same problem I found below solution.

class DataManager: Codable {
    var bases: [Base] = []
    init(bases: [Base]) {
        self.bases = bases
    }
    private enum CodingKeys: String, CodingKey {
        case bases
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        var list = try container.nestedUnkeyedContainer(forKey: DataManager.CodingKeys.bases)

        while !list.isAtEnd {
            if let child1 = try? list.decode(Child1.self) {
                bases.append(child1 as Base)
            } else if let child2 = try? list.decode(Child2.self) {
                bases.append(child2 as Base)
            }
        }
    }
}

Upvotes: 2

Related Questions