Reputation: 412
There are lots of APIs that respond with appropriate HTTP status code if something went wrong, but there are also APIs that always respond with 200 status code describing error in specific JSON fields. This is exactly my case. So I made a BasicResponse
protocol that requires any Codable
struct to have those mandatory fields:
protocol BasicResponse: Codable {
var response: String { get }
var message: String? { get }
}
My Codable
struct now looks like this:
struct GetTicketsResponse: Codable, BasicResponse {
var response: String
var message: String?
var tickets: [Ticket]
}
And I also created sort of middleware decoder to identify an error on early stages:
class AdvancedDecoder {
func decode<T: BasicResponse>(_ type: T.Type, from data: Data) throws -> T {
let result = try JSONDecoder().decode(T.self, from: data)
// Check if session is valid
if (result.message == "Session invalid") { throw AdvancedError.invalidSession }
return result
}
}
But I noticed a problem with this approach. If something fails on a server it returns JSON that have those response
and message
fields but does not have tickets
field, so decoding the response always fails. I could've marked tickets
array as an optional type var tickets: [Ticket]?
but it seems like a very strange workaround to me. Here are two JSONs for the reference:
// If everything went smoothly
{
"response" : "OK",
"tickets" : [...]
}
// If something went wrong
{
"response" : "ERROR",
"message" : "Session invalid"
}
So how do I handle this kind of scenarios? I bet there is a way better solution to this. Thanks in advance :)
Upvotes: 2
Views: 1489
Reputation: 285069
In my opinion an enum with associated types is the most efficient solution
enum Response : Decodable {
case success([Ticket])
case failure(String)
private enum CodingKeys : String, CodingKey { case response, message, tickets}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let response = try container.decode(String.self, forKey: .response)
switch response {
case "OK":
let ticketData = try container.decode([Ticket].self, forKey: .tickets)
self = .success(ticketData)
case "ERROR":
let errorMessage = try container.decode(String.self, forKey: .message)
self = .failure(errorMessage)
default:
throw DecodingError.dataCorruptedError(forKey: .response, in: container, debugDescription: "Invalid response string")
}
}
}
And use it
do {
let result = try JSONDecoder().decode(Response.self, from: data)
switch result {
case .success(let tickets): print(tickets)
case .failure(let errorMessage): print(errorMessage)
}
} catch {
print(error)
}
Upvotes: 1
Reputation: 16341
You could make two different Response
struct for success and failure. If success fails to decode try decoding with failure response.
struct SuccessResponse: Codable {
var tickets: [...]
}
struct FailureResponse: Codable {
var response, message: String
}
func decodingFunction(data: Data) {
do {
let response = try JSONDecoder().decode(SuccessResponse.self, from: data)
let tickets = response.tickets
//...
} catch {
do {
let failureResponse = try JSONDecoder().decode(FailureResponse.self, from: data)
let message = failureResponse.message
//...
} catch {
print(error.localizedDescription)
}
}
}
Upvotes: 0