How to decode error response message in Combine?

Hi! I'm doing login using SwiftUI and Combine. Could you please give me some idea how can I show json error when user types incorrect email or password? 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.

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
    }
}

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 will return token error, not what I need
                  }
              },
              receiveValue: { value in
                  print("Token:", value.token)
                 }
              })
            .store(in: &subscriptions)
    }

If I'm understanding this correctly, you need to first try to decode the data into LoginResponse; if that fails because the server returned an error payload, you need to try to decode the data into ErrorResponse. In that case, you can't use the decode operator. Just do this:

return URLSession.shared
    .dataTaskPublisher(for: request)
    .print()
    .receive(on: DispatchQueue.main)
    .tryMap { data, response -> LoginResponse in

        let decoder = JSONDecoder()

        // first try to decode `LoginResponse` from the data
        if let loginResponse = try? decoder.decode(
            LoginResponse.self, from: data
        ) {
            return loginResponse
        }
            
        // if that fails, then try to decode `ErrorResponse` and throw it as
        // an error
        throw try decoder.decode(ErrorResponse.self, from: data)

    }
    .eraseToAnyPublisher()

You must conform ErrorResponse to Error.

1 Like

Thank you! Yes, this's right solution. Some user from SO suggested this idea too and added:

receiveCompletion: { completion in
                  switch completion {
                  case .finished:
                      print("finished")
                  case .failure(let error as ErrorResponse):
                      print("Email/Password error:", error.message)
                      
                  case .failure(let error):
                      print("Token error", error)
                  }
              }

It's interesting that if we first indicate case .failure(let error) it will not decode error message correctly.

The order of the cases does not affect whether the error response is correctly decoded. By the time the sink receives the completion event, the error has either been decoded into ErrorResponse or not.

The problem with writing case .failure(let error): before case .failure(let error as ErrorResponse): is that the first case matches any Failure type, including ErrorResponse. Remember: switch cases are evaluated in the order they are written and only the first matching case is executed, even if more than one case would have matched.

1 Like