How to write the codable in generic format

Viewed 199

I have understand how to make the codable wrapper class for services response structure. But some times in server side the attribute value varies It may Int Or String.

Example

struct ResponseDataModel : Codable{
    let data : DataClass?
    enum  CodingKey: String, CodingKey{
        case data = "data"

    }
    init(from decoder: Decoder) throw {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        data = try values.decodeIfPresent(DataClass.self, forKey:.data)
    }
}

struct DataClass : Codable{
    let id : Int
    let name : String?
    let age : Int?

    enum  CodingKey: String, CodingKey{
        case id = "id"
        case name = "name"
        case age = "age"

    }
    init(from decoder: Decoder) throw {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(Int.self, forKey:.it)
        name = try values.decodeIfPresent(String.self, forKey:.name)
        age = try values.decodeIfPresent(Int.self, forKey:.age)
    }
}

I would like to use generic way if id int string no matter what its it should bind to my controller with id value data.

let id : <T>

How to write the codable in generic formate.

3 Answers

You can do that using the following model:

struct ResponseDataModel<T: Codable>: Codable{
    let data : DataClass<T>?
    enum  CodingKeys: String, CodingKey{
        case data = "data"

    }
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        data = try values.decodeIfPresent(DataClass<T>.self, forKey:.data)
    }
}

struct DataClass<T: Codable>: Codable {
    let id: T?
    let name: String?
    let age: Int?

    enum  CodingKeys: String, CodingKey{
        case id = "id"
        case name = "name"
        case age = "age"

    }
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(T.self, forKey:.id)
        name = try values.decodeIfPresent(String.self, forKey:.name)
        age = try values.decodeIfPresent(Int.self, forKey:.age)
    }
}

However, you should always known the type of the id property when you call decode(_:from:) function of JSONDecoder like this:

let decoder = JSONDecoder()

do {
    let decoded = try decoder.decode(ResponseDataModel<Int>.self, from: data)
    print(decoded)
} catch {
    print(error)
}

Or you can use the following model to always map the id as Int, even if your server sends it as String:

struct ResponseDataModel: Codable{
    let data : DataClass?
    enum  CodingKeys: String, CodingKey{
        case data = "data"

    }
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        data = try values.decodeIfPresent(DataClass.self, forKey:.data)
    }
}

struct DataClass: Codable {
    let id: Int?
    let name: String?
    let age: Int?

    enum  CodingKeys: String, CodingKey{
        case id = "id"
        case name = "name"
        case age = "age"

    }
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        do {
            id = try values.decodeIfPresent(Int.self, forKey:.id)
        } catch DecodingError.typeMismatch {
            if let idString = try values.decodeIfPresent(String.self, forKey:.id) {
                id = Int(idString)
            } else {
                id = nil
            }
        }
        name = try values.decodeIfPresent(String.self, forKey:.name)
        age = try values.decodeIfPresent(Int.self, forKey:.age)
    }
}

As per @Joakim Danielson example provided, you can reach desired result by attempting to decode value for each type.

struct Response: Decodable {
    let id: String
    let name: String?
    let age: Int?
    
    private enum CodingKeys: String, CodingKey {
        case data
    }
    
    private enum NestedCodingKeys: String, CodingKey {
        case id
        case name
        case age
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let nestedContainer = try container.nestedContainer(
            keyedBy: NestedCodingKeys.self,
            forKey: .data
        )
        
        if let id = try? nestedContainer.decode(Int.self, forKey: .id) {
            self.id = String(id)
        } else {
            id = try nestedContainer.decode(String.self, forKey: .id)
        }
        
        name = try nestedContainer.decodeIfPresent(String.self, forKey: .name)
        age = try nestedContainer.decodeIfPresent(Int.self, forKey: .age)
    }
}

As @gcharita illustrated you can also catch DecodingError, but do-catch statement for decode(_:forKey:) would act only as an early exit, since it throws one of the following errors - typeMismatch, keyNotFound or valueNotFound for that particular key-value pair.

First of all, here are some key points to take case when using Codable for parsing.

  1. There is no need to every time implement enum CodingKeys if the property names and keys have exactly same name.

  2. Also, no need to implement init(from:) if there no specific parsing requirements. Codable will handle all the parsing automatically if the models are written correctly as per the format.

So, with the above 2 improvements your ResponseDataModel looks like,

struct ResponseDataModel : Codable{
    let data: DataClass?
}

Now, for DataClass you simply need to add an if-else condition to handle the Int and String cases. Implementing generics is not needed here.

Use String or Int as the type for id. And add the conditions accordingly. In the below code, I'm using id as String.

struct DataClass : Codable {
    let id : String //here....
    let name : String?
    let age : Int?
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decodeIfPresent(String.self, forKey: .name)
        age = try values.decodeIfPresent(Int.self, forKey: .age)

        if let id = try? values.decode(Int.self, forKey: .id) {
            self.id = String(id)
        } else {
            self.id = try values.decode(String.self, forKey:.id)
        }
    }
}
Related