Search code examples
swiftgenericsjsondecoder

Swift json decoding fails when data is empty in function with generic types


I have written a function with generic type result.

private func fetch<Request, Response>(_ endpoint: Types.EndPoint,
                                          method: HTTPMethod = .get,
                                          body: Request? = nil,
                                          then callback: ((Result<Response, Types.Error>) -> Void)? = nil
    ) where Request: Encodable, Response: Decodable {

    //Network request creation...

    let dataTask = URLSession.shared
            .dataTask(with: urlRequest) { data, response, error in
                if let error = error {
                    print("Fetch error: \(error)")
                    callback?(.failure(.generic(reason: "Could not fetch data :\(error.localizedDescription)")))
                } else {
                                            
                    if let data = data {


                        do {
                            let result = try self.decoder.decode(Response.self, from: data) // **exception here**
                            callback?(.success(result))
                        } catch {
                            print("Decoding error: \(error)")
                            callback?(.failure(.generic(reason: "Could not decode data :\(error.localizedDescription)")))
                        }

                    }
                }
    }
}


......

apiClient.fetch(.updateUser, API.Types.Request.User) { (result: Result<API.Types.Response.Empty, API.Types.Error>) in
        
        //handle result
        
}

API.Types.Response.Empty is a struct,

struct Empty: Decodable {}

Now my question is, when above api is called, server returns an empty response (Content-Length is 0) when it is successful. (status-code is 200). only few apis do this, other apis return some kind of json with the response.

When response data is empty, json decoding throws an exception

Unable to parse empty data." UserInfo={NSDebugDescription=Unable to parse empty data

How do I properly handle this situation? Any help would be much appreciated.


Solution

  • This is because empty data is not a valid JSON. Here's a simple test:

    struct Empty: Codable {}
    var data = Data()
    // or var data = "".data(using: .utf8)!
    let result = try JSONDecoder().decode(Empty.self, from: data)
    

    will throw the same exception, and it actually contains the explanation:

    DecodingError
      ▿ dataCorrupted : Context
        - codingPath : 0 elements
        - debugDescription : "The given data was not valid JSON."
        ▿ underlyingError : Optional<Error>
          - some : Error Domain=NSCocoaErrorDomain Code=3840 "Unable to parse empty data." UserInfo={NSDebugDescription=Unable to parse empty data.}
    

    The valid JSON data that matches your model would look like {} (i.e. an object with no fields). This works:

    var data = "{}".data(using: .utf8)! // <-- empty JSON object
    let result = try JSONDecoder().decode(Empty.self, from: data)
    

    So next question is: is it your fault or API fault, and can you fix it by properly using the information in the headers:

    1. Are you always sending Accept: application/json header with your request?

      • if yes, clearly API doesn't respect it. It may be an API bug, unless API docs tell you to do something else (see point 2.)
      • if not, you may need to start sending it to tell the API to always send JSON back.
    2. When your API returns an empty data, what does it specify in content-type header?

      • If API still claims that it returned application/json, it's a bug in API.
      • If API sets content-type to something different in this case, not application/json, then you need to get this header from response (response.contentType) and parse it differently.

    If neither of the above options is available, then you have to do a workaround and just check if data size is 0. But that's suboptimal, because you are relying on some behaviour of the API, which may or may not be accidental.