0

I am hitting an endpoint that may return one of two different JSONs, depending on whether the user needs to answer a security question:

// possible response (payload) #1
{
    "token": "123lknk123kj1n13132"
}

// possible response (payload) #2
{
    "securityQuestion": "What is your mother's maiden name?"
}

My goal is to create a model that will decode the JSON differently based on which key is present in the payload (i.e. either "token" or "securityQuestion"). Currently, I am getting a parsing error and I don't know why.

I took inspiration from a very well-crafted answer to a previous question on SO. My current code is a modified version of it which (in theory) fits my need. I would like the final version of my code to retain this structure. My code is below:

/**
Enumerates the possible payloads received from Server

- success: Successful payload that contains the user's access token
- securityQuestion: Payload that contains the security question that the user has to answer to receive a token
*/
enum PayloadType: String, Decodable {
    case success
    case securityQuestion
}

protocol Payload: Decodable { static var payloadType: PayloadType { get } }

/// Model for successful response sent by the server.
struct SuccessfulResponse: Payload {
    static let payloadType = PayloadType.success
    let token: String
}

/// Model for response sent by the server which includes a security question
struct SecurityQuestionResponse: Payload {
    static let payloadType = PayloadType.securityQuestion
    let securityQuestion: String
}

/// Model for building a response sent by the server.
struct Response: Decodable {
    let data: Payload
    let payloadType: PayloadType

    init(from decoder: Decoder) throws {
        // NOTE*: This part is a little shaky, maybe this is where I am going wrong
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.payloadType = try values.decode(PayloadType.self, forKey: .payloadType)

        // payloadType will determine how the JSON is decoded
        switch self.payloadType
        {

        case .success:
            self.data = try values.decode(SuccessfulResonse.self, forKey: .data)

        case .securityQuestion:
            self.data = try values.decode(SecurityQuestionResponse.self, forKey: .data)
        }
    }

    private enum CodingKeys: String, CodingKey {
        case data
        case payloadType
    }
}

Before posting this question, I looked at various similar posts (1, 2, 3) but none of them really fit needs.

Swifty
  • 839
  • 2
  • 15
  • 40
  • Don't think this best for `Codable` you need to use SwiftyJSON/JSONSerialization 1 key doesn't deserve that headache – Shehata Gamal Jul 23 '19 at 19:13
  • You are right, it would not be worth it for just 1 key. The rationale behind this architecture is that there are actually multiple responses that the server can send, each of which has many keys. I decided not to list them in this post for the sake of brevity and to avoid overwhelming visitors with too much code. I would like to avoid external libraries if possible. Also I'm not well-versed in JSONSerialization and it would probably require me to change the architecture for this model. Would you be able to help me with the assumption that this structure is to remain the same? – Swifty Jul 23 '19 at 19:15
  • `struct Root:Codable { let token,securityQuestion:String?}` – Shehata Gamal Jul 23 '19 at 19:17
  • This was the first thing I tried. Making `securityQuestion`/`token` optional will not work. Not sure why – Swifty Jul 23 '19 at 19:18
  • in `catch { print(error)}` check the error – Shehata Gamal Jul 23 '19 at 19:19
  • also i think this is a messy response structure , it's better to build the response to be model re presentable with status/type key to know how to deal with – Shehata Gamal Jul 23 '19 at 19:30
  • Possible duplicate of [Dynamic JSON Decoding Swift 4](https://stackoverflow.com/questions/47603630/dynamic-json-decoding-swift-4) – koen Jul 23 '19 at 19:34
  • 1
    @DataDaddy if you have valid `JSON` data than please try from this URL https://app.quicktype.io/ – Vishal Parmar Jul 26 '19 at 12:57

1 Answers1

2

I would like to suggest a different approach, is not more simple but easy to use, al least more clear to me.

struct Token: Codable { 
   let token: String?
}

struct SecurityQuestion:Codable {
    let securityQuestion: String?
}

in the request function add the following

URLSession.shared.dataTask(with: url, completionHandler: {data,response,error in

    do {
        let responseObject = try JSONDecoder().decode(Token.self, from: data!)

        let token = responseObject.token
        print(token)
    } catch let parseError {
        print(parseError)
        do{
            let responseObject = try JSONDecoder().decode(SecurityQuestion.self, from: data!)
            let securityQuestion = responseObject.securityQuestion
            print(securityQuestion)
        }catch{
            print(error)
        }
    }
})

the main idea is to use the catch block to try another decode type since the first one failed, and if you have many different response you could be doing more catch block to do a new decode type,

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Yoel Jimenez del valle
  • 1,270
  • 1
  • 9
  • 20
  • This is a viable solution. I'm going to keep an open mind here and mark it as the accepted answer, although one thing I noticed is that if there are multiple keys (which I did not include in the post for the sake of brevity), then the nest of do-catch statements is going to get ugly pretty quickly – Swifty Jul 23 '19 at 20:02
  • that's true but for keys then it would be in the `init(from: Decode)` func i have a model where the decoded func where bigger then the struct definition because endpoint could return a string a doblue a int or a bool for same value, so if a painfull decode – Yoel Jimenez del valle Jul 23 '19 at 20:05