Reputation: 8012
There is an API, which wraps its responses in an associative array with a status
value and a data
value, where data
contains either an error object, or the expected values:
Bad Response:
{
"status":"error",
"data":{
"errormessage":"Duplicate entry '101' for key 'PRIMARY'",
"errorcode":1062
}
}
Successful response:
{
"status":"success",
"data":{
"user": {
"id": 1,
}
}
}
I want to validate these responses:
public class func validateResponse(_ data : Data) -> WebServicesError?
{
struct WTPResponse : Decodable
{
let status : String
let data : Data
}
do {
let response = try JSONDecoder().decode(WTPResponse.self, from: data) // FAILS HERE
if let wtpError = try? JSONDecoder().decode(WTPAPIError.self, from: response.data) {
return WebServicesError.wtpError(WTPAPIError(code: wtpError.code, message: wtpError.message))
}
}
catch let error {
return WebServicesError.init(error: error)
}
return nil
}
It always fails when trying to decode the response object with the error: Expected to decode Data but found a dictionary instead.
I was thinking that I could decode the data
object as the Swift type Data
, but it is really a [String: Any]
dictionary.
1) How can I validate the Data
I receive from the API?
2) Is there a way I can extract only the "data
" portion of the JSON response as the Data
type, so that I can decode the User
object without having to give it a status
and data
properties?
Upvotes: 1
Views: 3319
Reputation: 1604
I used quicktype's multi-source mode to generate separate Codable
models for each response type:
And here's the code. You can try to decode a Response
first, and, if that fails, you can try to decode a BadResponse
.
// let response = try? JSONDecoder().decode(Response.self, from: jsonData)
// let badResponse = try? JSONDecoder().decode(BadResponse.self, from: jsonData)
import Foundation
struct Response: Codable {
let status: String
let data: ResponseData
}
struct ResponseData: Codable {
let user: User
}
struct BadResponse: Codable {
let status: String
let data: BadResponseData
}
struct BadResponseData: Codable {
let errormessage: String
let errorcode: Int
}
struct User: Codable {
let id: Int
}
I think this is a bit neater than trying to express this as a single type. I also suggest not selectively decoding the JSON, and rather decoding all of it, then picking out the data you want from these types.
Upvotes: 1
Reputation: 386018
As the other answers state, you essentially can't do this with JSONDecoder
because you can't decode a Data
for your "data"
key. You'd need to decode it as a Dictionary<String, Any>
or something. I can think of a way to do that, but it's pretty cumbersome, and even then, you end up with a Dictionary
, not a Data
, so you'd have to re-encode it to get a Data
to pass to a JSONDecoder
later.
Maybe this means you have to drop down to the lower-level JSONSerialization
and poke through the dictionaries “by hand”. But if you know at decode-time exactly what kinds of responses you are looking for, then I suggest you work with the Swift Decodable
system instead of bypassing it.
At the top level, you have a response, which can either be a failure or a success, and carries a different data payload in each case. That sounds like a Swift enum
with associated values:
enum WTPResponse {
case failure(WTPFailure)
case success(WTPSuccess)
}
We want this to be decodable directly from the JSON, but we'll have to write the Decodable
conformance by hand. The compiler can't do it automatically for an enum
with associated values. Before we write the Decodable
conformance, let's define all the other types we'll need.
The type of response is identified by either the string "error"
or the string "success"
, which sounds like another Swift enum
. We can make this enum
a RawRepresentable
of String
, and then Swift can make it Decodable
for us:
enum WTPStatus: String, Decodable {
case error
case success
}
For the failure response type, the data payload has two fields. This sounds like a Swift struct
, and since the fields are String
and Int
, Swift can make it Decodable
for us:
struct WTPFailure: Decodable {
var errormessage: String
var errorcode: Int
}
For the success response type, the data payload is a user, which has an id: Int
field. This sounds like two Swift struct
s, which Swift can make Decodable
for us:
struct WTPSuccess: Decodable {
var user: WTPUser
}
struct WTPUser: Decodable {
var id: Int
}
This covers everything that appears in your example JSONs. Now we can make WTPResponse
conform to Decodable
by hand, like this:
extension WTPResponse: Decodable {
enum CodingKeys: String, CodingKey {
case status
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(WTPStatus.self, forKey: .status) {
case .error: self = .failure(try container.decode(WTPFailure.self, forKey: .data))
case .success: self = .success(try container.decode(WTPSuccess.self, forKey: .data))
}
}
}
Here's a test:
let failureJsonString = """
{
"status":"error",
"data":{
"errormessage":"Duplicate entry '101' for key 'PRIMARY'",
"errorcode":1062
}
}
"""
let successJsonString = """
{
"status":"success",
"data":{
"user": {
"id": 1,
}
}
}
"""
let decoder = JSONDecoder()
do {
print(try decoder.decode(WTPResponse.self, from: failureJsonString.data(using: .utf8)!))
print(try decoder.decode(WTPResponse.self, from: successJsonString.data(using: .utf8)!))
} catch {
print(error)
}
And here's the output:
failure(test.WTPFailure(errormessage: "Duplicate entry \'101\' for key \'PRIMARY\'", errorcode: 1062))
success(test.WTPSuccess(user: test.WTPUser(id: 1)))
Upvotes: 4
Reputation: 131501
I'm not sure how you'd go about doing this with the new Codable
feature, as shayegh says.
You could instead use the JSONSerialization
class. That would convert your JSON data to a Dictionary that contains other dictionaries. You could then interrogate the dictionary yourself through code.
That would be pretty easy.
Upvotes: 0
Reputation: 322
This is a case where Swift4 Codable doesn't work. You have to parse the JSON manually and take care of the cases. https://github.com/SwiftyJSON/SwiftyJSON
Upvotes: 0