How to handle two types of server responses to a single request in Swift


Greetings, traveler!

Once, I encountered a situation where the server responded with two different objects for the same request. It’s not the most convenient way to work, but it was the only option available. Let’s look at this example and see how we can find a solution to escape this situation.

Model

First, let’s create the models we will be using. There will be two models, one for each server response option.

struct First: Decodable {
    let name: String
}

struct Second: Decodable {
    let number: Int
}

Response Handler

Then, let’s create a simple Response Handler to convert Data into the needed object. We will use a generic function to do this.

enum NetworkError: Error {
    case parse
    case fetchData
}

struct ServerResponceHandler {
    
    func handle<T: Decodable>(
        _ data: Data,
        type: T.Type,
        completion: @escaping (Result<T, any Error>) -> Void
    ) {
        
        let result: Result<T, any Error>
        
        defer {
            completion(result)
        }
        
        guard let data = try? JSONDecoder().decode(type, from: data) else {
            result = .failure(NetworkError.parse)
            return
        }
        
        result = .success(data)
    }
    
}

By the way, did you notice how I called the completion in this example? This maneuver allows us to avoid forgetting to process one of the cases. You can read more about it here.

Request Manager

Now, let’s create a network layer Manager. This Manager will have a function that allows us to retrieve Data using a URLRequest.

final class RequestManager {
    
    private let queue = DispatchQueue(label: "request-queue", qos: .utility)
    
    private func fetchData(
        request: URLRequest,
        completion: @escaping (Result<Data, any Error>) -> Void
    ) {
        
        let configuration = URLSessionConfiguration.default
        configuration.urlCredentialStorage = nil
        
        let session = URLSession(configuration: configuration)
        
        queue.async {
            session.dataTask(with: request) { data, _, error in
                let result: Result<Data, any Error>
                
                defer {
                    completion(result)
                }
                
                guard let data else {
                    result = .failure(error ?? NetworkError.fetchData)
                    return
                }
                
                result = .success(data)
            }
        }
    }
    
}

Either

The server offers only two possible responses to this request. We can create a container to store each case to solve this issue in the code. Apple already has a convenient API that could be useful in this situation, but the problem is that it is not public. If so, let’s copy this solution.

enum Either<Left, Right> {
    case left(Left)
    case right(Right)
}

By the way, we have already used something similar to the one mentioned above. The Result type is also a container for two types; the difference is that the second generic type of a Result must conform to the Error protocol.

enum Result<Success, Failure> where Failure : Error {
    case success(Success)
    case failure(Failure)
}

Result

Let’s go back to our code. Create a function for our Manager that will allow us to process both server response cases. To do this, we will first get the Data, then try using our Response Handler to convert it into the first expected case of the model. If this does not work, we will try to convert it to the second option. If an error is waiting for us here, too, we will pass it in the closure. If any of the model cases parse, we will transfer it inside the case of our Either-container.

extension RequestManager {
        
    func fetchEither<Left: Decodable, Right: Decodable>(
        urlRequest: URLRequest,
        completion: @escaping (Result<Either<Left, Right>, any Error>) -> Void
    ) {
        
        let handler = ServerResponceHandler()
        
        fetchData(request: urlRequest) { result in
            
            switch result {
            case .success(let data):
            
            		// First try
                handler.handle(data, type: Left.self) { result in
                    switch result {
                    case .success(let value):
                        completion(.success(.left(value)))
                        
                    case .failure(let failure):
                    
                    		// Second try
                        handler.handle(data, type: Right.self) { result in
                            switch result {
                            case .success(let value):
                                completion(.success(.right(value)))
                            case .failure(let error):
                                completion(.failure(error))
                            }
                        }
                    }
                }
                
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
    
}

Let’s create an API Service that uses our Manager to retrieve two specific data types.

final class APIService {
    
    private let manager = RequestManager()
    
    func fetchResponce(_ completion: @escaping (Result<Either<First, Second>, any Error>) -> Void) {
        var request = URLRequest(url: URL(string: "https://livsycode.com/")!)
        request.httpMethod = "GET"
        
        manager.fetchEither(urlRequest: request, completion: completion)
    }
    
}

Then, let’s create a ViewModel that uses this service and processes each result differently.

final class ViewModel {
    
    private let service = APIService()
    
    func fetchResponce() {
        service.fetchResponce { [weak self] result in
            switch result {
            case .success(let value):
                self?.handleResponce(value)
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
    
    private func handleResponce(_ value: Either<First, Second>) {
        switch value {
        case .left(let first):
            print(first.name)
        case .right(let second):
            print(second.number)
        }
    }
    
}

Conclusion

We found a solution to an unusual situation and, at the same time, learned a technique that could be useful in this case and many others as well.