jovanjovanovic
jovanjovanovic

Reputation: 4178

Swift Decodable, Endpoint returns completely different types

With API I'm working with, I have a case where 1 API Endpoint can return completely different responses, based on if the call was successful or not.
In case of success, API Endpoint returns an Array of requested objects, in the root, something like this:

[
    {
        "key1": "value1",
        "key2": "value2",
        "key3": "value3"
    },
    {
        "key1": "value1",
        "key2": "value2",
        "key3": "value3"
    },
    ...
]

which I'm normally decoding with try JSONDecoder().decode([Object].self, from: data)

In case of an error, API Endpoint returns something completely different, looks like this:

{
    "error": "value1",
    "message": "value2",
    "status": "value3"
}

and decoding with try JSONDecoder().decode([Object].self, from: data) normally fails.

Now, my question is, is there a way, to decode error response keys, in this kind of (I would say not so normally architectured API), WITHOUT creating a -what I call- plural object named Objects that would have optional properties error, message, status, and for example objects.
My thinking got somewhere to extending Array where Element == Object and somehow trying to decode error, message, status, but I'm hitting Conformance of 'Array<Element>' to protocol 'Decodable' was already stated in the type's module 'Swift'. Maybe it's not even possible to do it that way, so any other, even completely different, suggestion would be very welcome.

Upvotes: 1

Views: 1075

Answers (4)

vadian
vadian

Reputation: 285220

My suggestion is to decode the root object of the JSON as enum with associated values

struct Item : Decodable {
    let key1, key2, key3 : String
}

struct ResponseError  : Decodable {
    let error, message, status : String
}

enum Response : Decodable {
    case success([Item]), failure(ResponseError)
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = .success(try container.decode([Item].self))
        } catch DecodingError.typeMismatch {
            self = .failure(try container.decode(ResponseError.self))
        }
    }
}

and use it

do {
    let result = try JSONDecoder().decode(Response.self, from: data)
    switch result {
        case .success(let items): print(items)
        case .failure(let error): print(error.message)
    }
} catch {
    print(error)
}

It's good practice to catch only the specific .typeMismatch error and hand over other errors instantly to the caller.

Upvotes: 3

Joakim Danielson
Joakim Danielson

Reputation: 52043

Introduce an "abstract" struct that is the receiver of the decode call and let that struct decode the correct type and return a Result object

enum ApiErrorEnum: Error {
    case error(ApiError)
}

struct ResponseHandler: Decodable {
    let result: Result<[ApiResult], ApiErrorEnum>

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        do {
            let values = try container.decode([ApiResult].self)
            result = .success(values)
        } catch {
            let apiError = try container.decode(ApiError.self)
            result = .failure(.error(apiError))
        }
    }
}

and it could then be used for instance using a closure

func decodeApi(_ data: Data, completion: @escaping (Result<[ApiResult], ApiErrorEnum>?, Error?) -> ()) {
    do {
        let decoded = try JSONDecoder().decode(ResponseHandler.self, from: data)
        completion(decoded.result, nil)
    } catch {
        completion(nil, error)
    }
}

Upvotes: 1

flanker
flanker

Reputation: 4210

Utilise a do-catch block to allow you try decoding one type, and if that fails try the other option. I quite like to use an enum to handle the result...

struct Opt1: Codable {
   let key1, key2, key3: String
}

struct Opt2: Codable {
   let error, message, status: String
}

enum Output {
   case success([Opt1])
   case failure(Opt2)
}

let decoder = JSONDecoder()
let data = json.data(using: .utf8)!
var output: Output

do {
   let opt1Array = try decoder.decode([Opt1].self, from: data)
   output = .success(opt1Array)
} catch {
   let opt2 = try decoder.decode(Opt2.self, from: data)
   output = .failure(opt2)
}

Upvotes: 0

EmilioPelaez
EmilioPelaez

Reputation: 19912

You can try to decode [Object] and if that fails, decode another struct with your error keys.

Upvotes: 1

Related Questions