GoldenJoe
GoldenJoe

Reputation: 8012

How to validate this JSON in Swift4?

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

Answers (4)

David Siegel
David Siegel

Reputation: 1604

I used quicktype's multi-source mode to generate separate Codable models for each response type:

multi mode

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

rob mayoff
rob mayoff

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 structs, 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

Duncan C
Duncan C

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

shayegh
shayegh

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

Related Questions