Search code examples
swiftswift4decodabledo-catch

Catch pattern changes callback signature


I am trying to use JSONDecoder to decode a json response from my server using Alamofire. When I decode the response with a guard, it works without any issues. The side-effect of this approach is that I can't tell what the issue is when the decode actually fails.

guard let result: TResponseData = try? decoder.decode(TResponseData.self, from: response.data!) else {
    self.logger.error("Unable to decode the response data into a model representation.")
    return
}

So instead I'm wanting to use a do { } catch { } but I can't figure out how exactly I'm supposed to use it within the Alamofire responseJSON callback.

This is what I've currently got:

Alamofire.request(completeUrl, method: .post, parameters: parameters, encoding: encoding, headers: headers)
.validate()
 .responseJSON { (response) -> Void in
    self.logger.info("POST Response: \(String(describing:response.response?.statusCode))")
    switch response.result {
    case .success(_):
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .custom(Date.toTMDBDate)

        do {
            let _ = try decoder.decode(TResponseData.self, from: response.data!)
        } catch DecodingError.dataCorrupted(let error) {
            self.logger.error(error.underlyingError)
            return
        }

        completion(result)
        return
    case .failure(let error):
      //....
    }

What I am given with this code however is a compiler error on the .responseJSON { (response) -> Void in line.

Invalid conversion from throwing function of type '(_) -> Void' to non-throwing function type '(DataResponse) -> Void'.

The guard code works fine, and if I change the try to a try? or force an unwrap, it compiles - I just don't get to have my catch handle the actual error.

If I change the catch block so that it does not include any pattern, then the code compiles.

catch {
    return
}

This doesn't give me anything over what my guard was giving me. I really want to capture the error encountered with the decode operation. Am I using the wrong pattern? Why does using the DecodingError.dataCorrupted pattern seemingly change the callback signature?


Solution

  • JSONDecoder can throw errors other than DecodingError.dataCorrupted; you need to be able to handle the case of an arbitrary Error being thrown. So, if you want to handle that error, you'll want an unconditional catch {} block.

    You can also:

    • Use responseData instead of responseJSON as you're doing your own deserialisation with JSONDecoder.
    • Use the unwrap() method on Alamofire's Result type in order to coalesce the network error with the decoding error, if desired.

    This is what that looks like:

    Alamofire
        .request(
            completeUrl, method: .post, parameters: parameters,
            encoding: encoding, headers: headers
        )
        .validate()
        .responseData { response in
    
            self.logger.info(
                "POST Response: \(response.response?.statusCode as Any)"
            )
    
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .custom(Date.toTMDBDate)
    
            do {
                let result = try decoder.decode(
                    TResponseData.self, from: response.result.unwrap()
                )
                completion(result)
            } catch {
                self.logger.error(error)
            }
        } 
    

    Although one thing to note here is that you're not calling completion if the request fails; I would personally change that such that you do, and propagate the error back by having the completion take a Result<TResponseData> parameter.

    In that case, you can use Result's flatMap(_:) method rather than unwrap() and a catch {} block:

    func doRequest(_ completion: @escaping (Result<TResponseData>) -> Void) {
    
        let completeURL = // ...
        let parameters =  // ...
        let encoding =    // ...
        let headers =     // ...
    
        Alamofire
            .request(
                completeURL, method: .post, parameters: parameters,
                encoding: encoding, headers: headers
            )
            .validate()
            .responseData { response in
    
                self.logger.info(
                    "POST Response: \(response.response?.statusCode as Any)"
                )
    
                let decoder = JSONDecoder()
                decoder.dateDecodingStrategy = .custom(Date.toTMDBDate)
    
                // if response.result is successful data, try to decode.
                // if decoding fails, result is that error.
                // if response.result is failure, result is that error.
                let result = response.result.flatMap {
                    try decoder.decode(TResponseData.self, from: $0)
                }
                .ifFailure {
                    self.logger.error($0)
                }
    
                completion(result)
            }
    }