Swift Decodable, Endpoint returns completely different types
Asked Answered
L

4

1

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.

Leucippus answered 10/11, 2019 at 20:43 Comment(0)
B
3

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.

Bowes answered 11/11, 2019 at 8:33 Comment(0)
S
1

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

Scull answered 10/11, 2019 at 20:50 Comment(1)
thanks for your response, but it wouldn't work in some cases. For example, with Generics, when the whole set of Networking Classes (like APIRouter) is expecting to receive a type of an object that should be decoded. Like let objectsAPI = APIRouter<[Object]>()Leucippus
U
1

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)
    }
}
Unlikely answered 10/11, 2019 at 21:8 Comment(1)
The use of Result is the right way to go. I'd recommend you get rid of the first variant completely, or at least change it into an enum.Bellis
L
0

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)
}
Lunula answered 10/11, 2019 at 21:25 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.