简体   繁体   中英

Swift struct optional values

I am adding a simple login system to my SwiftUI project. Only I can't quite figure it out.

What the problem is, when a user wants to login and it works. I get this response from the server:

    "user": {
        "id": 6,
        "name": "test",
        "email": "test@test.com",
        "email_verified_at": null,
        "created_at": "2020-07-02T09:37:54.000000Z",
        "updated_at": "2020-07-02T09:37:54.000000Z"
    },
    "assessToken": "test-token"
} 

But when something isn't right, the server displays an error message like this:

    "message": "The given data was invalid.",
    "errors": {
        "email": [
            "The email field is required."
        ],
        "password": [
            "The password field is required."
        ]
    }
}

How can I make sure I parse this information into a structure. At the moment it looks like this.

// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
//   let welcome = try? newJSONDecoder().decode(Welcome.self, from: jsonData)

import Foundation

// MARK: - Welcome
struct Login: Codable {
    let user: User
    let assessToken: String
}

// MARK: - User
struct User: Codable {
    let id: Int
    let name, email: String
    let emailVerifiedAt: JSONNull?
    let createdAt, updatedAt: String
    
    enum CodingKeys: String, CodingKey {
        case id, name, email
        case emailVerifiedAt = "email_verified_at"
        case createdAt = "created_at"
        case updatedAt = "updated_at"
    }
}

// MARK: - Encode/decode helpers

class JSONNull: Codable, Hashable {
    
    public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
        return true
    }
    
    public var hashValue: Int {
        return 0
    }
    
    public init() {}
    
    public required init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if !container.decodeNil() {
            throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
        }
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encodeNil()
    }
}

This is how i do it now:

class HttpAuth: ObservableObject{
    var didChange = PassthroughSubject<HttpAuth, Never>()
    
    var authenticated = false{
        didSet{
            didChange.send(self)
        }
    }
    
    func checkDetails(email: String, password: String){
        guard let url = URL(string: "https://test.ngrok.io/api/login") else {
            return
        }
        
        let body : [String : String] = ["email" : email, "password": password]
        
        let finalBody = try! JSONSerialization.data(withJSONObject: body)
        
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = finalBody
        
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            
            
            guard let data = data else {return}
            let finalData = try! JSONDecoder().decode(Login.self, from: data)
                
            
            
            print(finalData)
        }.resume()
    }
}

Do I have to create a new struct named like LoginError for example, or do I need it inside the existing login struct?

You need to create separate Codable models for both the success and error cases. And then combine them into a single model that you can use for parsing.

Login model:

struct Login: Decodable {
    let user: User
    let assessToken: String
}

struct User: Decodable {
    let id: Int
    let name, email: String
    let emailVerifiedAt: String?
    let createdAt, updatedAt: String
}

Error model:

struct ErrorResponse: Decodable {
    let message: String
    let errors: Errors
}

struct Errors: Decodable {
    let email, password: [String]
}

Combine the Login and ErrorResponse models into Response like so,

enum Response: Decodable {
    case success(Login)
    case failure(ErrorResponse)
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            let user = try container.decode(Login.self)
            self = .success(user)
        } catch  {
            let error = try container.decode(ErrorResponse)
            self = .failure(error)
        }
    }
}

Now, use Response model to parse your data like so,

URLSession.shared.dataTask(with: request) { (data, response, error) in
    guard let data = data else {return}
    do {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        let response = try decoder.decode(Response.self, from: data)
        switch response {
        case .success(let login):
            let assessToken = login.assessToken
            print(assessToken)
            //get any required data from login here..
            
        case .failure(let error):
            let message = error.message
        }
    } catch {
        print(error)
    }
}.resume()

Or use SwiftyJSON.

If third-party dependency is your concern. Write one yourself.

SwiftyJSON is basically one struct, namely JSON, to refactor out common operations.

The idea behind is simple:

JSON data is dynamic, Codable is largely static.

You don't usually have the luxury of immutable JSON response, hell, API itself changes all the time during development.

You also need to create Codable structs recursively with extra handling of special coding keys.

Codable only makes sense when in small scale or it's auto-gen.

Take another answer for example, you need to define 5 types for a JSON response. Most of them are not reusable.

With JSON, it is var j = JSON(data) .

guard let user = j[.user].string else {// error handling}...

where you replace string user with an enum case case user .

JSON, is reusable, coding key .user is reusable.

Since JSON is nested, you can replace user.id with j[.user][.id].stringValue or j[.user][.id].intValue depending on use case.

And again .user , .id can be added to coding keys enum to be reusable.

You also have great flexibility to adjust to run-time variations.

Ironically, in my opinion, one should use Codable for Encodable, not for Decodable.

Because when you encode, you have full control of its type; when you decode json response, you are at the mercy of backend.

Swift type system is great, but you don't always need the exact type at every step.

JSON is invaluable in deeply nested json response, eg; j[.result][.data][0][.user][.hobbies][0][.hobbyName].stringValue . When I'm done, you are still writing first level Codable struct.

Share this here as a comparison so more could appreciate how insanely powerful this is.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM