Decoding two different JSON responses in one model class using Codable

Viewed 1310

Based on the requirement I got two different kinds of response from api. That is

{
  "shopname":"xxx",
  "quantity":4,
  "id":1,
  "price":200.00,
}

another response

{
  "storename":"xxx",
  "qty":4,
  "id":1,
  "amount":200.00,
}

Here both json values are decoding in same model class. Kindly help me to resolve this concern.

is it is possible to set value in single variable like qty and quantity both are stored in same variable based on key param availability

3 Answers

Here's an approach that lets you have only one property in your code, instead of two Optionals:

Define a struct that contains all the properties you need, with the names that you'd like to use in your code. Then, define two CodingKey enums that map those properties to the two different JSON formats and implement a custom initializer:

let json1 = """
{
    "shopname":"xxx",
    "quantity":4,
    "id":1,
    "price":200.00,
}
""".data(using: .utf8)!

let json2 = """
{
    "storename":"xxx",
    "qty":4,
    "id":1,
    "amount":200.00,
}
""".data(using: .utf8)!

struct DecodingError: Error {}

struct Model: Decodable {
    let storename: String
    let quantity: Int
    let id: Int
    let price: Double

    enum CodingKeys1: String, CodingKey {
        case storename = "shopname"
        case quantity
        case id
        case price
    }

    enum CodingKeys2: String, CodingKey {
        case storename
        case quantity = "qty"
        case id
        case price = "amount"
    }

    init(from decoder: Decoder) throws {
        let container1 = try decoder.container(keyedBy: CodingKeys1.self)
        let container2 = try decoder.container(keyedBy: CodingKeys2.self)

        if let storename = try container1.decodeIfPresent(String.self, forKey: CodingKeys1.storename) {
            self.storename = storename
            self.quantity = try container1.decode(Int.self, forKey: CodingKeys1.quantity)
            self.id = try container1.decode(Int.self, forKey: CodingKeys1.id)
            self.price = try container1.decode(Double.self, forKey: CodingKeys1.price)
        } else if let storename = try container2.decodeIfPresent(String.self, forKey: CodingKeys2.storename) {
            self.storename = storename
            self.quantity = try container2.decode(Int.self, forKey: CodingKeys2.quantity)
            self.id = try container2.decode(Int.self, forKey: CodingKeys2.id)
            self.price = try container2.decode(Double.self, forKey: CodingKeys2.price)
        } else {
            throw DecodingError()
        }
    }
}


do {
    let j1 = try JSONDecoder().decode(Model.self, from: json1)
    print(j1)
    let j2 = try JSONDecoder().decode(Model.self, from: json2)
    print(j2)
} catch {
    print(error)
}

Handling different key names in single model

Below are two sample json(dictionaries) that have some common keys (one, two) and a few different keys (which serve the same purpose of error).

Sample json:

let error_json:[String: Any] = [
    "error_code": 404,                  //different
    "error_message": "file not found",  //different
    "one":1, //common
    "two":2  //common
]
let failure_json:[String: Any] = [
    "failure_code": 404,                 //different
    "failure_message": "file not found", //different
    "one":1, //common
    "two":2  //common
]

CommonModel

struct CommonModel : Decodable {
    var code: Int?
    var message: String?
    var one:Int  //common
    var two:Int? //common
    
    private enum CodingKeys: String, CodingKey{ //common
        case one, two
    }
    private enum Error_CodingKeys : String, CodingKey {
        case  code = "error_code", message = "error_message"
    }
    private enum Failure_CodingKeys : String, CodingKey {
        case  code = "failure_code", message = "failure_message"
    }
    init(from decoder: Decoder) throws {
        let commonValues =  try decoder.container(keyedBy: CodingKeys.self)
        let errors = try decoder.container(keyedBy: Error_CodingKeys.self)
        let failures = try decoder.container(keyedBy: Failure_CodingKeys.self)
        
        ///common
        self.one = try commonValues.decodeIfPresent(Int.self, forKey: .one)!
        self.two = try commonValues.decodeIfPresent(Int.self, forKey: .two)
        /// different
        if errors.allKeys.count > 0{
            self.code = try errors.decodeIfPresent(Int.self, forKey: .code)
            self.message = try errors.decodeIfPresent(String.self, forKey: .message)
        }
        if failures.allKeys.count > 0{
            self.code = try failures.decodeIfPresent(Int.self, forKey: .code)
            self.message = try failures.decodeIfPresent(String.self, forKey: .message)
        }
    }
}

Below extension will help you to convert your dictionary to data.

public extension Decodable {
    init(from: Any) throws {
        let data = try JSONSerialization.data(withJSONObject: from, options: .prettyPrinted)
        let decoder = JSONDecoder()
        self = try decoder.decode(Self.self, from: data)
    }
}

Testing

public func Test_codeble(){
    do {
         let err_obj = try CommonModel(from: error_json)
         print(err_obj)
         let failed_obj = try CommonModel(from: failure_json)
         print(failed_obj)
        
    }catch let error {
        print(error.localizedDescription)
    }
}

Use like

struct modelClass : Codable {

    let amount : Float?
    let id : Int?
    let price : Float?
    let qty : Int?
    let quantity : Int?
    let shopname : String?
    let storename : String?


    enum CodingKeys: String, CodingKey {
        case amount = "amount"
        case id = "id"
        case price = "price"
        case qty = "qty"
        case quantity = "quantity"
        case shopname = "shopname"
        case storename = "storename"
    }
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        amount = try values.decodeIfPresent(Float.self, forKey: .amount)
        id = try values.decodeIfPresent(Int.self, forKey: .id)
        price = try values.decodeIfPresent(Float.self, forKey: .price)
        qty = try values.decodeIfPresent(Int.self, forKey: .qty)
        quantity = try values.decodeIfPresent(Int.self, forKey: .quantity)
        shopname = try values.decodeIfPresent(String.self, forKey: .shopname)
        storename = try values.decodeIfPresent(String.self, forKey: .storename)
    }


}
Related