Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to decode a JSON array using Decodable?

I am requesting the API to send me some data which I can successfully retrieve yet I am stuck in the decoding process of it . here is the JSON I receive :

[  
   {  
      "challenge_id":1,
      "challenge_title":"newchallenge1",
      "challenge_pts_earned":1000,
      "challenge_description":"description1",
      "start_date":"2017-09-24T00:00:00.000Z",
      "end_date":"2017-09-24T00:00:00.000Z",
      "challenge_category_id":1,
      "status_id":2,
      "createdAt":"2017-09-24T17:21:47.000Z",
      "updatedAt":"2017-09-24T09:40:34.000Z"
   },
   {  
      "challenge_id":2,
      "challenge_title":"challenge1",
      "challenge_pts_earned":100,
      "challenge_description":"description1",
      "start_date":"2017-09-24T00:00:00.000Z",
      "end_date":"2017-09-24T00:00:00.000Z",
      "challenge_category_id":1,
      "status_id":0,
      "createdAt":"2017-09-24T17:22:12.000Z",
      "updatedAt":"2017-09-24T09:22:12.000Z"
   },
   {  
      "challenge_id":3,
      "challenge_title":"new eat title",
      "challenge_pts_earned":600000,
      "challenge_description":"haha",
      "start_date":"2017-01-09T00:00:00.000Z",
      "end_date":"2017-01-10T00:00:00.000Z",
      "challenge_category_id":2,
      "status_id":0,
      "createdAt":"2017-09-27T17:12:10.000Z",
      "updatedAt":"2017-09-27T09:15:19.000Z"
   }
]

and I am trying to create the following structure to decode it :

   struct challenge : Codable {
    let  id : String?
    let  title : String?
    let  pointsEarned : String?
    let  description : String?
    let  dayStarted : String?
    let  dayEnded : String?
    let  categoryID : String?
    let  statusID : Int?
    let createdAt : Date?
    let updatedAt : Date?

    enum CodingKeys: String, CodingKey {
        case id = "challenge_id"
        case title = "challenge_title"
        case pointsEarned = "challenge_pts_earned"
        case description = "challenge_description"
        case dayStarted = "start_date"
        case dayEnded = "end_date"
        case categoryID = "challenge_category_id"
        case statusID = "status_id"
        case createdAt, updatedAt
    }

}

And here is my code for the implementation :

 var All_challenges : [challenge]?
 let url = URL(string: API.all_challenges.rawValue)!
        var request = URLRequest(url: url)
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "GET"
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data, error == nil else {
                print("error=\(String(describing: error))")
                return
            }
            if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
                print("statusCode should be 200, but is \(httpStatus.statusCode)")
                print("\(String(describing: response))")
            }
            let responseString = String(data: data, encoding: .utf8)
            guard let result = responseString else { return }
            print(result)
           if let json = try? JSONDecoder().decode([challenge].self , from : data ) {
                self.All_challenges = json

           }

         }
        task.resume()

yet when I try to debug it I can never enter the if statement for

if let json = try? JSONDecoder().decode([challenge].self,from:data ) {
self.All_challenges = json

 }

Please give me some description on where my mistake is , I am very new to JSON paring

like image 923
Danial Kosarifa Avatar asked Dec 12 '17 12:12

Danial Kosarifa


1 Answers

Please catch the error and read it

Type 'String' mismatch.
Debug Description: Expected to decode String but found a number instead.

You get that error for challenge_id, challenge_pts_earned, challenge_category_id and status_id because the values are Int (actually you can notice that already when reading the JSON)

Secondly, the Date values cannot be decoded because you didn't provide a date strategy (the default is TimeInterval). You have to provide a custom date formatter to decode ISO8601 with fractional seconds.

Finally, as mentioned in the comments use camelCased variable names by specifying CodingKeys and as the JSON contains always all keys declare the properties as non-optional

let jsonString = """
[
{
"challenge_id":1,
"challenge_title":"newchallenge1",
"challenge_pts_earned":1000,
"challenge_description":"description1",
"start_date":"2017-09-24T00:00:00.000Z",
"end_date":"2017-09-24T00:00:00.000Z",
"challenge_category_id":1,
"status_id":2,
"createdAt":"2017-09-24T17:21:47.000Z",
"updatedAt":"2017-09-24T09:40:34.000Z"
},
{
"challenge_id":2,
"challenge_title":"challenge1",
"challenge_pts_earned":100,
"challenge_description":"description1",
"start_date":"2017-09-24T00:00:00.000Z",
"end_date":"2017-09-24T00:00:00.000Z",
"challenge_category_id":1,
"status_id":0,
"createdAt":"2017-09-24T17:22:12.000Z",
"updatedAt":"2017-09-24T09:22:12.000Z"
},
{
"challenge_id":3,
"challenge_title":"new eat title",
"challenge_pts_earned":600000,
"challenge_description":"haha",
"start_date":"2017-01-09T00:00:00.000Z",
"end_date":"2017-01-10T00:00:00.000Z",
"challenge_category_id":2,
"status_id":0,
"createdAt":"2017-09-27T17:12:10.000Z",
"updatedAt":"2017-09-27T09:15:19.000Z"
}
]
"""

struct Challenge : Decodable {

//    private enum CodingKeys : String, CodingKey {
//        case challengeId = "challenge_id"
//        case challengeTitle = "challenge_title"
//        case challengePtsEarned = "challenge_pts_earned"
//        case challengeDescription = "challenge_description"
//        case startDate = "start_date"
//        case endDate = "end_date"
//        case challengeCategoryId = "challenge_category_id"
//        case statusId = "status_id"
//        case createdAt, updatedAt
//    }

    let challengeId : Int
    let challengeTitle : String
    let challengePtsEarned : Int
    let challengeDescription : String
    let startDate : String // or Date
    let endDate : String // or Date
    let challengeCategoryId : Int
    let statusId : Int
    let createdAt : Date
    let updatedAt : Date
}

let data = Data(jsonString.utf8)
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
decoder.keyDecodingStrategy = .convertFromSnakeCase 
do {
    let challenges = try decoder.decode([Challenge].self, from: data)
    print(challenges)
} catch { print(error) }

Note:

While developing JSON en-/decoding it's highly recommended to use the full set of error handling. It makes debugging so much easier

do {
   try JSONDecoder().decode ...

} catch DecodingError.dataCorrupted(let context) {
    print(context)
} catch DecodingError.keyNotFound(let key, let context) {
    print("Key '\(key)' not found:", context.debugDescription)
    print("codingPath:", context.codingPath)
} catch DecodingError.valueNotFound(let value, let context) {
    print("Value '\(value)' not found:", context.debugDescription)
    print("codingPath:", context.codingPath)
} catch DecodingError.typeMismatch(let type, let context)  {
    print("Type '\(type)' mismatch:", context.debugDescription)
    print("codingPath:", context.codingPath)
} catch {
    print("error: ", error)
}

Edit

In Swift 4.1 and higher you can omit the CodingKeys by adding the keyDecodingStrategy

decoder.keyDecodingStrategy = .convertFromSnakeCase 
like image 158
vadian Avatar answered Oct 21 '22 18:10

vadian