1

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.

1 Answers1

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"
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks Rob! I'm still having an issue. Even after making the change you suggested, it's still not working. Is there something else I have to do if the JSON data I'm pulling is a root level array? – Cory B. Cromer Jan 20 '22 at 22:23
  • No, there's nothing special. Decoding `[Event].self` should be enough. So, when you looked at the error that `decode` threw, what did it say? It will tell you precisely where it is stumbling... – Rob Jan 20 '22 at 22:30
  • debugDescription: "Expected to decode Array but found a dictionary instead." – Cory B. Cromer Jan 20 '22 at 22:49
  • That doesn’t add up. The only only array that we’re expecting is that top level one, but you have assured us that this is what your JSON looks like. But your error is telling us that it actually found a dictionary. I’d suggest you confirm that the JSON is as you’ve shown in your question. E.g., `print(String(data: data, encoding: .utf8))`. – Rob Jan 20 '22 at 23:26
  • 1
    I got it. Thanks so much! – Cory B. Cromer Jan 20 '22 at 23:26