Home > Software design >  Swift Decode Array of Dictionaries inside String using Decodable Protocol
Swift Decode Array of Dictionaries inside String using Decodable Protocol

Time:03-31

The JSON response is:-

{ 
  "id" = "1"
  "message" = "SUCCESS"
  "data" = "[{\"name\":"FirstName",\"office_id\":1111,\"days_name\":\"Mon\"},
            {\"name\":"SecondName:,\"office_id\":1112,\"days_name\":\"Tue\"}]"

}

I don't seems to understand how to approach decoding "data", In data model shall the data be declared as String? and I have been trying to figure out but don't seem to get any clue and stuck here for a while, if anyone can please shed some light to it, it would help a lot. The only problem I am facing is how to deal with "" double quotes wrapped around data array as shown in above response.

Data model and URLSession code is below:

struct Root : Codable {
    let id : String?
    let message : String?
    let data : String?

    enum CodingKeys: String, CodingKey {

        case id = "id"
        case message = "message"
        case data = "data"
    }
}

struct insideData: Codable {
    
    let name: String?
    let officeId : Int?
    let daysName: String?
    
    enum CodingKeys: String, CodingKey {

        case name = "name"
        case officeId = "office_id"
        case daysName = "days_name"
   
    }
}

URLSession.shared.dataTask(with: url!) { (responseData, httpUrlResponse , error) in
        
    if(error == nil && responseData != nil && responseData?.count != 0){
        
        let decoder = JSONDecoder()
        
        do{
            
            let result = try decoder.decode(Root.self, from: responseData!)
            print(result.data!)

        }
        catch let error {
            debugPrint("Error occured while decoding = \(error.localizedDescription)")
        }
    }
}.resume()


I save result.data! in a new variable and convert it to data and again use JSONDecoder but now with insideData.self struct but don't get desired output, not getting mapped with keys inside insideData struct. I am just getting started with learning networking in swift so please pardon me for silly mistakes.

CodePudding user response:

data value is JSON with JSON, ie it's a JSONString.

A way to parse it is to parse again the JSON String. To do so, you need to override init(from decoder:) of Root.

Let's first fix your JSON which isn't valid, to be able to use it in Playgrounds.

let jsonString = #"""
{
    "id": "1",
    "message": "SUCCESS",
    "data": "[{\"name\":\"FirstName\",\"office_id\":1111,\"days_name\":\"Mon\"}, {\"name\":\"SecondName:\", \"office_id\":1112,\"days_name\":\"Tue\"}]"
}
"""#

Then, change data: let data : [InsideData]

You could then:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decodeIfPresent(String.self, forKey: .id)
    self.message = try container.decodeIfPresent(String.self, forKey: .message)
    guard let dataString = try container.decodeIfPresent(String.self, forKey: .data) else {
        self.data = []
        return
    }
    self.data = try JSONDecoder().decode([InsideData].self, from: Data(dataString.utf8))
}

If you don't like creating a new decoder, you can pass one in userInfo of the decoder:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decodeIfPresent(String.self, forKey: .id)
    self.message = try container.decodeIfPresent(String.self, forKey: .message)
    guard let dataString = try container.decodeIfPresent(String.self, forKey: .data) else {
        self.data = []
        return
    }
    guard let insideDecoder = decoder.userInfo[CodingUserInfoKey(rawValue: "InsideDecoder")!] as? JSONDecoder else {
        self.data = []
        return
    }
    self.data = try insideDecoder.decode([InsideData].self, from: Data(dataString.utf8))
}

Then the root decoding:

let decoder = JSONDecoder()
let insideDecoder = JSONDecoder()
decoder.userInfo = [CodingUserInfoKey(rawValue: "InsideDecoder")!: insideDecoder]
do {
    let root = try decoder.decode(Root.self, from: Data(jsonString.utf8))
    print(root)
} catch {
    print("Error: \(error)")
}

CodePudding user response:

The provided JSON looks to be invalid.

  1. In json you don't use the = sign but the : sign between a key and value.
  2. You are missing , behind each value.
  3. There is a typo behind SecondName :, should be ",
  4. It's weird to have quotes around your data array. It can be easier decoded when you have them removed.

Suggested JSON changes:

{
  "id": "1",
  "message": "SUCCESS",
  "data": [
    {"name":"FirstName","office_id":1111,"days_name":"Mon"},
    {"name":"SecondName","office_id":1112,"days_name":"Tue"}
  ]
}

I've tested decoding this json in Playground and it seems to work:

struct DataEntity: Decodable {
    let name: String
    let officeId: Int
    let daysName: String
    
    enum CodingKeys: String, CodingKey {
        case name = "name"
        case officeId = "office_id"
        case daysName = "days_name"
    }
}

struct RootEntity: Decodable {
    let id: String
    let message: String
    let data: [DataEntity]
}

struct Mapper {
    func map(json: String) -> RootEntity {
        guard let rootJsonData = json.data(using: .utf8) else {
            fatalError("Couldn't convert json string to data")
        }
        do {
            let rootEntity = try JSONDecoder().decode(RootEntity.self, from: rootJsonData)
            return rootEntity
        } catch let error {
            fatalError(error.localizedDescription)
        }
    }
}

let jsonToDecode = """
{
  "id": "1",
  "message": "SUCCESS",
  "data": [
    {"name":"FirstName","office_id":1111,"days_name":"Mon"},
    {"name":"SecondName","office_id":1112,"days_name":"Tue"}
  ]
}
"""

let rootEntity = Mapper().map(json: jsonToDecode)
print(rootEntity)

Print output:

RootEntity(id: "1", message: "SUCCESS", data: [DataEntity(name: "FirstName", officeId: 1111, daysName: "Mon"), DataEntity(name: "SecondName", officeId: 1112, daysName: "Tue")])
  • Related