Trouble Decoding JSON Data with Swift

Trying to get a little practice in decoding JSON data, and I am having a problem. I know the URL is valid, but for some reason my decoder keeps throwing an error. Below is my model struct, the JSON object I'm trying to decode, and my decoder.

Model Struct:

struct Event: Identifiable, Decodable {
    let id: Int
    let description: String
    let title: String
    let timestamp: String
    let image: String
    let phone: String
    let date: String
    let locationline1: String
    let locationline2: String
}

struct EventResponse: Decodable {
    let request: [Event]
}

JSON Response:

[
  {
    "id": 1,
    "description": "Rebel Forces spotted on Hoth. Quell their rebellion for the Empire.",
    "title": "Stop Rebel Forces",
    "timestamp": "2015-06-18T17:02:02.614Z",
    "image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Battle_of_Hoth.jpg",
    "date": "2015-06-18T23:30:00.000Z",
    "locationline1": "Hoth",
    "locationline2": "Anoat System"
  },
  {
    "id": 2,
    "description": "All force-sensitive members of the Empire must report to the Sith Academy on Korriban. Test your passion, attain power, to defeat your enemy on the way to becoming a Dark Lord of the Sith",
    "title": "Sith Academy Orientation",
    "timestamp": "2015-06-18T21:52:42.865Z",
    "image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Korriban_Valley_TOR.jpg",
    "phone": "1 (800) 545-5334",
    "date": "2015-09-27T15:00:00.000Z",
    "locationline1": "Korriban",
    "locationline2": "Horuset System"
  },
  {
    "id": 3,
    "description": "There is trade dispute between the Trade Federation and the outlying systems of the Galactic Republic, which has led to a blockade of the small planet of Naboo. You must smuggle supplies and rations to citizens of Naboo through the blockade of Trade Federation Battleships",
    "title": "Run the Naboo Blockade",
    "timestamp": "2015-06-26T03:50:54.161Z",
    "image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Blockade.jpg",
    "phone": "1 (949) 172-0789",
    "date": "2015-07-12T19:08:00.000Z",
    "locationline1": "Naboo",
    "locationline2": "Naboo System"
  }
]

My Decoder:

func getEvents(completed: @escaping (Result<[Event], APError>) -> Void) {
        guard let url = URL(string: eventURL) else {
            completed(.failure(.invalidURL))
            return
        }
               
        let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, response, error in
            
            if let _ =  error {
                completed(.failure(.unableToComplete))
                return
            }
                        
            guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
                completed(.failure(.invalidResponse))
                return
            }
            
            guard let data = data else {
                completed(.failure(.invalidData))
                return
            }
            
            do {
                let decoder = JSONDecoder()
                let decodedResponse = try decoder.decode(EventResponse.self, from: data)
                completed(.success(decodedResponse.request))
            } catch {
                completed(.failure(.invalidData))
            }
        }
        
        task.resume()
    }

I am sure the answer is pretty obvious to some but I have been beating my head against a wall. Thanks.


Solution 1:

The EventResponse suggests that the JSON will be of the form:

{
    "request": [...]
}

But that is obviously not what your JSON contains.

But you can replace:

let decodedResponse = try decoder.decode(EventResponse.self, from: data)

With:

let decodedResponse = try decoder.decode([Event].self, from: data)

And the EventResponse type is no longer needed.


FWIW, in the catch block, you are returning a .invalidData error. But the error that was thrown by decode(_:from:) includes meaning information about the parsing problem. I would suggest capturing/displaying that original error, as it will tell you exactly why it failed. Either print the error message in the catch block, or include the original error as an associated value in the invalidData error. But as it stands, you are discarding all of the useful information included in the error thrown by decode(_:from:).


Unrelated, but you might change Event to use URL and Date types:

struct Event: Identifiable, Decodable {
    let id: Int
    let description: String
    let title: String
    let timestamp: Date
    let image: URL
    let phone: String
    let date: Date
    let locationline1: String
    let locationline2: String
}

And configure your date formatted to parse those dates for you:

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)    // not necessary because you have timezone in the date string, but useful if you ever use this formatter with `JSONEncoder`
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSX"

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)

And if you want to have your Swift code follow camelCase naming conventions, even when the API does not, you can manually specify your coding keys:

struct Event: Identifiable, Decodable {
    let id: Int
    let description: String
    let title: String
    let timestamp: Date
    let image: URL
    let phone: String
    let date: Date
    let locationLine1: String
    let locationLine2: String

    enum CodingKeys: String, CodingKey {
        case id, description, title, timestamp, image, phone, date
        case locationLine1 = "locationline1"
        case locationLine2 = "locationline2"
    }
}