Search code examples
swiftdecode

Decode JSON from the nested container and check its type dynamically for typecasting in swift


this is my JSON, which I want to decode. It have array of objects in screen, which is of different type, So I want to cast each object depending on it's objectid. e.g if it's object id is bd_label then it should be of type LabelConfig.

{
    "objectid": "template",
    "version": {
        "major": "2",
        "minor": "1"
    },
    "screens":[
        {
            "id":"1",
            "objectid":"bd_label",
            "height": "100",
            "width" : "50",
            "label": "it's a label"
            
        },
        {
            "id":"2",
            "objectid":"bd_input",
            "height": "100",
            "width" : "50",
            "placeholder": "enter your input"
        },
        {
            "id":"3",
            "objectid":"bd_button",
            "height":"100",
            "width" : "50",
            "btn_label":
            [
                "click",
                " the",
                " button"
            ]
            
        }
    ]
}

I want to decode it, For that I have tried Following Strucure:

struct Version : Codable{
    var major : String
    var minor : String
}

protocol ComponentConfig: class, Codable{
    var id : String { get set }
    var objectid : String { get set }
}

class LabelConfig : ComponentConfig {
    var id: String
    var objectid : String
    var height : String?
    var width : String?
    var label : String?
}

class ButtonConfig : ComponentConfig {
    var id: String
    var objectid : String
    var height : String?
    var width : String?
    var btn_label : [String]
}

class InputConfig : ComponentConfig {
    var id: String
    var objectid : String
    var height : String?
    var width : String?
    var placeholder : String?
}

Here, and I want to decide what type of UIConfig i.e. LabelConfig or ButtonConfig or InputConfig to decode dynamically depending on objectid property of that object.

struct ScreenData: Decodable {
    
    var objectid : String
    var version: Version
    var screens : [ComponentConfig]
    
    enum CodingKeys: CodingKey {
        case objectid, version, screens
    }

 init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        objectid = try container.decode(String.self, forKey: .objectid)
        version = try container.decode(Version.self, forKey: .version)
      }
} 

Solution

  • It is simple to do using inheritance

    import SwiftUI
    class CustomJSONViewModel: ObservableObject {
        var configObject: ConfigObject?{
            do {
                let decodedResponse = try JSONDecoder().decode(ConfigObject.self, from: jsonData!)
                return decodedResponse
            } catch {
                print(error)
                return nil
            }
        }
        
        let jsonData = """
        {
            "objectid": "template",
            "version": {
                "major": "2",
                "minor": "1"
            },
            "screens":[
                {
                    "id":"1",
                    "objectid":"bd_label",
                    "height": "100",
                    "width" : "50",
                    "label": "it's a label"
                    
                },
                {
                    "id":"2",
                    "objectid":"bd_input",
                    "height": "100",
                    "width" : "50",
                    "placeholder": "enter your input"
                },
                {
                    "id":"3",
                    "objectid":"bd_button",
                    "height":"100",
                    "width" : "50",
                    "btn_label":
                    [
                        "click",
                        " the",
                        " button"
                    ]
                    
                }
            ]
        }
        """.data(using: .utf8)
    }
    struct CustomJSONView: View {
        @StateObject var vm: CustomJSONViewModel = CustomJSONViewModel()
        var body: some View {
            List{
                Text(vm.configObject?.objectid ?? "nil")
                Text(vm.configObject?.version.major ?? "nil")
                Text(vm.configObject?.version.minor ?? "nil")
                if vm.configObject?.screens != nil{
                    ForEach(vm.configObject!.screens, id: \.id){ screen in
                        //To access the specific properties you have to detect the type
                        if screen is BDInput{
                            //Once you know what type it is you can force it into that type
                            Text((screen as! BDInput).placeholder ?? "nil placeholder").foregroundColor(.green)
                        }else if screen is BDLabel{
                            Text((screen as! BDLabel).label ?? "nil label").foregroundColor(.orange)
                        }else if screen is BDButton{
                            Text((screen as! BDButton).btnLabel?.first ?? "nil button").foregroundColor(.blue)
                        }else{
                            //You would default to Screen if the type in unknown 
                            Text(screen.objectid)
                        }
                    }
                }
            }
        }
    }
    // MARK: - ConfigObject
    class ConfigObject: Codable {
        let objectid: String
        let version: Version
        let screens: [Screen]
        init(objectid: String, version: Version, screens: [Screen]) {
            self.objectid = objectid
            self.version = version
            self.screens = screens
        }
        required public init(from decoder: Decoder) throws {
            do{
                let coder = try decoder.container(keyedBy: CodingKeys.self)
                self.objectid = try coder.decode(String.self, forKey: .objectid)
                self.version = try coder.decode(Version.self, forKey: .version)
                //https://stackoverflow.com/questions/64182273/how-to-decode-an-array-of-inherited-classes-in-swift
                let container = try decoder.container(keyedBy: CodingKeys.self)
                var objectsArray = try container.nestedUnkeyedContainer(forKey: CodingKeys.screens)
                var items = [Screen]()
                
                var array = objectsArray
                while !objectsArray.isAtEnd {
                    let object = try objectsArray.nestedContainer(keyedBy: Screen.CodingKeys.self)
                    let type = try object.decode(String.self, forKey: Screen.CodingKeys.objectid)
                    //Here is where the decision is made to decode as the specific type/objectid
                    switch type {
                    case "bd_label":
                        items.append(try array.decode(BDLabel.self))
                    case "bd_input":
                        items.append(try array.decode(BDInput.self))
                    case "bd_button":
                        items.append(try array.decode(BDButton.self))
                    default:
                        items.append(try array.decode(Screen.self))
                    }
                }
                self.screens = items
            }catch{
                print(error)
                throw error
            }
        }
        enum CodingKeys: String, CodingKey {
            case objectid, version, screens
        }
    }
    
    // MARK: - Screen
    ///The different types will be a Screen
    class Screen: Codable {
        let id, objectid, height, width: String
        enum CodingKeys: String, CodingKey {
            case id, objectid, height, width
        }
        
        init(id: String, objectid: String, height: String, width: String) {
            self.id = id
            self.objectid = objectid
            self.height = height
            self.width = width
        }
        //This superclass would do all the work for the common properties
        required public init(from decoder: Decoder) throws {
            do{
                let coder = try decoder.container(keyedBy: CodingKeys.self)
                self.id = try coder.decode(String.self, forKey: .id)
                self.objectid = try coder.decode(String.self, forKey: .objectid)
                self.height = try coder.decode(String.self, forKey: .height)
                self.width = try coder.decode(String.self, forKey: .width)
            }catch{
                print(error)
                throw error
            }
        }
        
    }
    // MARK: - BDLabel
    class BDLabel: Screen {
        //Each subclass would handle their individual properties
        let label: String?
        init(id: String, objectid: String, height: String, width: String, label: String?) {
            self.label = label
            super.init(id: id, objectid: objectid, height: height, width: width)
        }
        //Each subclass would handle their individual properties
        required public init(from decoder: Decoder) throws {
            do{
                let coder = try decoder.container(keyedBy: CodingKeys.self)
                self.label = try coder.decode(String.self, forKey: .label)
                //Sending the super class work to be done by Screen
                try super.init(from: decoder)
            }catch{
                print(error)
                throw error
            }
        }
        enum CodingKeys: String, CodingKey {
            case  label
        }
    }
    // MARK: - BDInput
    class BDInput: Screen {
        //Each subclass would handle their individual properties
        let placeholder: String?
        init(id: String, objectid: String, height: String, width: String, placeholder: String?) {
            self.placeholder = placeholder
            //Sending the super class work to be done by Screen
            super.init(id: id, objectid: objectid, height: height, width: width)
        }
        required public init(from decoder: Decoder) throws {
            do{
                let coder = try decoder.container(keyedBy: CodingKeys.self)
                self.placeholder = try coder.decode(String.self, forKey: .placeholder)
                try super.init(from: decoder)
            }catch{
                print(error)
                throw error
            }
        }
        enum CodingKeys: String, CodingKey {
            case placeholder
        }
    }
    // MARK: - BDButton
    class BDButton: Screen {
        //Each subclass would handle their individual properties
        let btnLabel: [String]?
        init(id: String, objectid: String, height: String, width: String, btnLabel: [String]?) {
            self.btnLabel = btnLabel 
            //Sending the super class work to be done by Screen
            super.init(id: id, objectid: objectid, height: height, width: width)
        }
        required public init(from decoder: Decoder) throws {
            do{
                let coder = try decoder.container(keyedBy: CodingKeys.self)
                self.btnLabel = try coder.decode([String].self, forKey: .btnLabel)
                //Sending the super class work to be done by Screen
                try super.init(from: decoder)
            }catch{
                print(error)
                throw error
            }
        }
        enum CodingKeys: String, CodingKey {
            case btnLabel = "btn_label"
        }
    }
    // MARK: - Version
    struct Version: Codable {
        let major, minor: String
    }
    
    struct CustomJSONView_Previews: PreviewProvider {
        static var previews: some View {
            CustomJSONView()
        }
    }