4
\$\begingroup\$

I'm working with a REST service that responds with indeterminate types. In particular, it can respond with a String, a correct payload (T: Decodable) or an error payload (PayloadError).

In order to cope with this type of requirement I've ended up with the following solution:

do {
    let decoder = JSONDecoder()
    let either = try decoder.decode(Either<T, PayloadError>.self, from: data)
    switch either {
        case .left(let value):
            completion(.success(value))
        case .right(let payloadError):
            completion(.failure(.generic)))
    }
} catch {
    if let value = String(data: data, encoding: .utf8) as? T {
        completion(.success(value))
    } else {
        completion(.failure(.generic))
    }
}

where Either is defined as:

enum Either<T, U> {
    case left(T)
    case right(U)
}

extension Either: Decodable where T: Decodable, U: Decodable {
    init(from decoder: Decoder) throws {
        if let value = try? T(from: decoder) {
            self = .left(value)
        } else if let value = try? U(from: decoder) {
            self = .right(value)
        } else {
            let context = DecodingError.Context(
                codingPath: decoder.codingPath,
                debugDescription: "Cannot decode \(T.self) or \(U.self)"
            )
            throw DecodingError.dataCorrupted(context)
        }
    }
}

Do you think the code looks good or should I use a more elegant solution?

Update 01.11.2020

These are the possible use cases along side with concrete responses.

Use Case 1

The client calls API method /authenticate. The server can responds with an authentication token of type String or a payload error of type PayloadError (if the server responds with unauthorized, for example). The JSON used to decode PayloadError is something like:

{
    code: 1,
    message: "Error message here"
}

PayloadError is defined as:

struct PayloadError: Decodable {
    let code: Int
    let message: String
}

Use Case 2

The client calls any other method. For example, it can call /user that gives back the info for the current user (the auth token is passed along). A struct User: Decodable is decoded from the returned JSON. If there is an error, the server replies with a PayloadError.

Based on the above use cases, the server can respond with:

  • String
  • T: Decodable (like User)
  • PayloadError

What I don't like is the code in catch since its intent is not clear.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Can you provide an example of concrete responses of the various types, and the corresponding T and PayloadError types? – I wonder how String(data: data, encoding: .utf8) as? T works if T is anything but String. \$\endgroup\$ – Martin R Oct 31 '20 at 19:30
  • 1
    \$\begingroup\$ @MartinR thanks for your comment. I've added an update as you requested. Hope it's clear. \$\endgroup\$ – Lorenzo B Nov 1 '20 at 10:41
  • \$\begingroup\$ I assume that .generic is from your custom error type? Perhaps you can add the signature of the completion function (including the used types), just to avoid misunderstandings. – As far as I can see, there is a general problem: If a string is expected, then any response which is not a well-formed PayloadError will be interpreted as a string. \$\endgroup\$ – Martin R Nov 3 '20 at 11:12
  • \$\begingroup\$ @MartinR sorry for the delay. I've finally end up splitting the two use cases. See below. Thanks. \$\endgroup\$ – Lorenzo B Nov 5 '20 at 11:59
0
\$\begingroup\$

I've finally end up splitting the two use cases. This in order to make the code readable and more maintainable.

PayloadError is mapped to a ServiceError: Error in order to align the output.

Use case 1

guard let data = response.data else {
    completion(.failure(.invalidData))
    return
}

let jsonDecoder = JSONDecoder()
if let payloadError = try? jsonDecoder.decode(PayloadError.self, from: data) {
    completion(.failure(ServiceError.from(payloadError)))
} else if let token = String(data: data, encoding: .utf8) {
    completion(.success(token))
} else {
    completion(.failure(.decodingError))
}

Use case 2

guard let data = response.data else {
    completion(.failure(.invalidData))
    return
}
let decoder = JSONDecoder()
guard let either = try? decoder.decode(Either<T, PayloadError>.self, from: data) else {
    completion(.failure(.decodingError))
    return
}
switch either {
case .left(let value):
    completion(.success(value))
case .right(let payloadError):
    completion(.failure(ServiceError.from(payloadError)))
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.