Swift Parsing decode 2 different json with 1 url api

Viewed 371

Hi im new in swift and im kinda still learning, so i try to make login controller and parse a json data if it corrects it parse a json data with id and stuff and if login is failed than the json will show a kinda message. i already make a struct for all the value data that required but i got this error that said its nil.

so, this is the json if the login is success :

[ { "id": 891, "name": "User", "email": "qdpim@immobisp.com", "status": "1" } ]

and this is the json if login is failed :

[ { "message": "Login Failed..", "status": "0" } ]

so basicly it has a same url i guess? but i dont know im kinda stuck in here and i need help

struct login : Codable {
    let id : Int
    let name : String
    let email : String
    let status : String
    let message : String

    init(dictionary : [String : Any]) {
        id = (dictionary ["id"] as? Int)!
        name = (dictionary ["name"] as? String)!
        email = (dictionary ["email"] as? String)!
        status = (dictionary ["status"] as? String)!
        message = (dictionary ["message"] as? String)!
    }

    enum CodingKeys : String, CodingKey {
        case id = "id"
        case name = "name"
        case email = "email"
        case status = "status"
        case message = "message"
    }
}

func Login() {

    let Email = EmailField.text!
    let Pass = PasswordField.text!


    print(api)

    guard let JsonUrl = URL(string: api) else {return}
    URLSession.shared.dataTask(with: JsonUrl) { (data, response, error) in
        guard let data = data else {return}

        do{
            let parsing = try JSONDecoder().decode([login].self, from: data)
            print(parsing)
            self.Loginnn = parsing
            let stats = self.Loginnn.map { $0.status}

            if stats.contains("1"){
                print("Login Success")
                DispatchQueue.main.async {
                    self.appDelegate.loginSeque()
                }
            }else if stats.contains("0") {
                let action = UIAlertAction(title: "Got It", style:             .default, handler: nil)
                let alert = UIAlertController(title: "Wrong Email /   Password", message: "Please Try Again ", preferredStyle: .alert)
                alert.addAction(action)
                self.present(alert, animated: true, completion: nil)

                // so basicly i wanna run this alert action by search status if its contains "0"
            }
        }
    }catch{
        print(error)
    }
}.resume()
}

so when i try to test to failed my login, i doesnt show the message in my json in my log, instead it show this error

"keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"id\", intValue: nil) (\"id\").", underlyingError: nil))"

i just wanna pop some message or alert if the login is failed because or wrong password or email.....so maybe can someone help me how to do it the best way?

4 Answers

You can declare Success and Failure response types as below,

struct LoginSuccess: Decodable {
    var id: Int
    var name: String
    var email: String
    var status: String
}

struct LoginFailure: Decodable {
    var status: String
    var message: String
}

and then use as,

guard let JsonUrl = URL(string: api) else { return }
URLSession.shared.dataTask(with: JsonUrl) { (data, response, error) in
    guard let data = data else { return }

        if let success = try? JSONDecoder().decode([LoginSuccess].self, from: data).first {
            GlobalVariable.UserId = String(success.id)
            DispatchQueue.main.async {                    
                 self.appDelegate.loginSeque()
            }
        } else if let failure = try? JSONDecoder().decode([LoginFailure].self, from: data).first {
            let action = UIAlertAction(title: "Got It", style:             .default, handler: nil)
            let alert = UIAlertController(title: "Wrong Email /   Password", message: failure.message, preferredStyle: .alert)
            alert.addAction(action)
            self.present(alert, animated: true, completion: nil)
        }
}.resume()

The success response only contains the keys ("id", "name", "email", "status")

[ { "id": 891, "name": "User", "email": "qdpim@immobisp.com", "status": "1" } ]

and the failure response only contains the keys ("message", "status")

[ { "message": "Login Failed..", "status": "0" } ]

If you want to use the same struct for both JSON responses, you should make the properties optional

struct login : Codable {
    var id: Int?
    var name: String?
    var email: String?
    var status: String?
    var message: String?
}

Also, since your keys are the same as your properties, you don't need enum CodingKeys or init for that matter if you use JSONDecoder().decode

In this situation I would use JSONSerialization to decode the data to a [[String: Any]] and look at the content to determine what kind of message it is.

In my code I have assumed the "status" item tells us if it was a successful login or not but one could for instance look for the presence of "id" or the count of elements in the dictionary as well to determine the type of response

do {
    let result = try JSONSerialization.jsonObject(with: data) as! [[String: Any]]
    if let response = result.first, let status = response["status"] as? String  {
        if status == "1" {
            if let id = response["id"] as? Int {
                let ids = String(id)
                //...
            }
        } else {
            if let message = response["message"] as? String {
                print(message)
            }
        }
    }
} catch {
    print(error)
}

Below is my solution used in the code from your question. Note that I have simplified the Login struct since it is only used when login was successful

struct Login  {
    let id : Int
    let name : String
    let email : String
}

do {
    let result = try JSONSerialization.jsonObject(with: data) as! [[String: Any]]
    if let response = result.first, let status = response["status"] as? String  {
        if status == "1" {
            //handle success
            let login = Login(id: response["id"] as? Int ?? 0,
                              name: response["name"] as? String ?? "",
                              email: response["email"] as? String ?? "")
            self.Loginnn = login
            DispatchQueue.main.async {
                self.appDelegate.loginSeque()
            }
        } else {
            let action = UIAlertAction(title: "Got It", style: .default, handler: nil)
            let alert = UIAlertController(title: "Wrong Email /   Password", message: "Please Try Again ", preferredStyle: .alert)
            alert.addAction(action)
            self.present(alert, animated: true, completion: nil)
        }
    }
} catch {
    print(error)
}

You've already got an answer (or three) for this, but I want to show you how to do it without using JSONSerialization or speculative decoding.

So we have some LoginSuccess and LoginFailure types that you want to decode:

struct LoginSuccess: Decodable {
    var id: Int
    var name: String
    var email: String
}

struct LoginFailure: Decodable {
    var message: String
}

And we want to discriminate between them based on a status that is in the same container as the fields of those types. So we create an enum:

enum LoginResult: Decodable {
    case success(LoginSuccess)
    case failure(LoginFailure)

    enum Keys: CodingKey {
        case status
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Keys.self)
        if try container.decode(String.self, forKey: .status) == "1" {
            self = .success(try LoginSuccess(from: decoder))
        } else {
            self = .failure(try LoginFailure(from: decoder))
        }
    }
}

Note that the enum's init does not call decoder.decode(LoginSuccess.self). It passes the decoder it was given to the LoginSuccess initializer. Same with LoginFailure. This means those initializers will extract values from the same container as the status field.

Test:

let successData = #"[ { "id": 891, "name": "User", "email": "qdpim@immobisp.com", "status": "1" } ]"#.data(using: .utf8)!
print(try JSONDecoder().decode([LoginResult].self, from: successData))

// Output:
[__lldb_expr_1.LoginResult.success(__lldb_expr_1.LoginSuccess(id: 891, name: "User", email: "qdpim@immobisp.com"))]

let failureData = #"[ { "message": "Login Failed..", "status": "0" } ]"#.data(using: .utf8)!
print(try JSONDecoder().decode([LoginResult].self, from: failureData))

// Output:
[__lldb_expr_1.LoginResult.failure(__lldb_expr_1.LoginFailure(message: "Login Failed.."))]

Note that because your example data is wrapped in [...], I decoded arrays of LoginResult.

Related