Search code examples
swiftcombine

How to decode error response message in Combine?


I'm doing login using SwiftUI and Combine. Could you please give me some idea how can I decode and show json error when user types incorrect email or password? I can only get token.

When I'm doing the same login request with incorrect email or password, server returns me this error message:

{
"code": "[jwt_auth] incorrect_password",
"message": "Incorrect password!",
"data": {
    "status": 403
}

}

The problem is that I can't understand how can I decode two different json responses when doing one request in Combine? I can only get token.

Here's model for login request:

struct LoginResponse: Decodable {
let token: String }

struct ErrorResponse: Decodable {
    let message: String
}
struct Login: Codable {
    let username: String
    let password: String
}

static func login(email: String, password: String) -> AnyPublisher<LoginResponse, Error> {
    let url = MarketplaceAPI.jwtAuth!
    var request = URLRequest(url: url)

    let encoder = JSONEncoder()
    let login = Login(username: email, password: password)
    let jsonData = try? encoder.encode(login)
    
    request.httpBody = jsonData
    request.httpMethod = HTTPMethod.POST.rawValue
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    return URLSession.shared
        .dataTaskPublisher(for: request)
        .print()
        .receive(on: DispatchQueue.main)
        .map(\.data)
        .decode(
          type: LoginResponse.self,
          decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

And in viewModel:

MarketplaceAPI.login(email: email, password: password)
        .sink(
          receiveCompletion: { completion in
              switch completion {
              case .finished:
                  print("finished")
              case .failure(let error):
                  print("Failure error:", error.localizedDescription) // This's returning token error 
              }
          },
          receiveValue: { value in
              print("Token:", value.token)
             }
          })
        .store(in: &subscriptions)
}

Solution

  • I would make ErrorResponse conform to the Error protocol:

    struct ErrorResponse: Decodable, Error {
        let message: String
    }
    

    Then, use tryMap instead of decode (which is sort of a special case of tryMap).

    .tryMap({ data -> LoginResponse in
        let decoder = JSONDecoder()
        guard let loginResponse = try? decoder.decode(LoginResponse.self, from: data) else {
            throw try decoder.decode(ErrorResponse.self, from: data)
        }
        return loginResponse
    })
    

    First, try to decode the data as a LoginResponse. Note the use of try? here. This is so that we can check whether this has failed or not. If this fails, we throw an error. The error we throw is the data decoded as an ErrorResponse, or whatever error is thrown during the decoding of that.

    In your view model, you can check the error like this:

    .sink { completion in
        switch completion {
        case .failure(let error as ErrorResponse):
            // wrong password/username
            // you can access error.message here
        case .failure(let error):
            // some other sort of error:
        default:
            break
        }
    } receiveValue: { loginResponse in
        ...
    }