decoding a json to generic array or class in swift

2019-08-14 06:03发布

问题:

How do you decode json to a generic model in swift?
In java for decoding json I use GSON and in general it does not matter I use <T<E>> or ArrayList<E>.In swift Array is a struct and can't be inheritance and it has not implemented Decodable.

I'm looking for a generic elegant class to use in all my web service.

My scenario:
I have json response

{
"status": true,
"message": "",
"code": 200,
"response": [{
    "id": 43
}]
}

and a generic reponse model like this from web services:

class GeneralResponse< T : Decodable >:NSObject,Decodable{

    var status = false
    var message = ""
    var code = -1
    var response : T?

    private enum CodingKeys: String, CodingKey {
        case status
        case message
        case code
        case response
    }

    required public init(from decoder: Decoder) throws{
        let container = try decoder.container(keyedBy: CodingKeys.self)
        status = try container.decode(Bool.self, forKey: .status)
        message = try container.decode(String.self, forKey: .message)
        code = try container.decode(Int.self, forKey: .code)
        response = try container.decode(T.self, forKey: .response)
    }

}
class ItemDemoModel:Decodable {
     var id = -1
    private enum ItemDemModelCodingKeys : String, CodingKey {
        case id
    }
     required init(from decoder:Decoder) throws {
        let container = try decoder.container(keyedBy: ItemDemModelCodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
    }
}

response variable can be ItemDemoModel or an array of ItemDemoModel.
For example:
It can be GeneralResponse<Array<ItemDemoModel>>> or GeneralResponse<ItemDemoModel>>

thanks.

回答1:

If you declare a Decodable properties with same name as the key in json then you don't really need an enum to define Coding keys and an initializer to manually map every property with the key.

Also, there is no need to inherit from NSObject in Swift until you have a specific use case for that. Looking at the declaration, it seems unnecessary so your GeneralResponse can be redeclared as simple as this,

class GeneralResponse<T: Decodable>: Decodable {

    var code: Int
    var status: Bool
    var message: String?
    var response : T?
}

Similarly, ItemDemoModel can be declared as this,

class ItemDemoModel: Decodable {
     var id: Int
}

Now you can setup your service as below to get the GeneralResponse<T> for any request,

struct RequestObject {
    var method: String
    var path: String
    var params: [String: Any]
}

class WebService {

    private let decoder: JSONDecoder

    public init(_ decoder: JSONDecoder = JSONDecoder()) {
        self.decoder = decoder
    }

    public func decoded<T: Decodable>(_ objectType: T.Type,
                                      with request: RequestObject,
                                      completion: @escaping  (GeneralResponse<T>?, Error?) -> Void)  {
        // Here you should get data from the network call. 
        // For compilation, we can create an empty object.
        let data = Data()

        // Now parsing
        do {
            let response  = try self.decoder.decode(GeneralResponse<T>.self, from: data)
            completion(response, nil)
        } catch {
            completion(nil, error)
        }
    }
}

Usage

let request = RequestObject(method: "GET", path: "https://url.com", params: [:])
WebService().decoded([ItemDemoModel].self, with: request) { (response, error) in
    if let items = response?.response {
        print(items)
    }
}

P.S; You must be used to declare arrays and dictionaries as below,

let array: Array<SomeType>
let dictionary: Dictionary<String: SomeType>
let arrayOfDictionary: Array<Dictionary<String: SomeType>>

But with Swift's type inference, you can declare an array and a dictionary as simple as below,

let array: [SomeType]
let dictionary: [String: SomeType]
let arrayOfDictionary: [[String: SomeType]]


回答2:

Array<T> conforms to Decodable if T conforms to Decodable, so GeneralResponse<[ItemDemoModel]> won't produce any errors.

As shown here:

You can simply do this:

let decoder = JSONDecoder()
let obj = try decoder.decode(type, from: json.data(using: .utf8)!)


回答3:

Here you have a function you may want to use in order to decode your JSON:

func decode<T: Decodable>(_ data: Data, completion: @escaping ((T) -> Void)) {
    do {
        let model = try JSONDecoder().decode(T.self, from: data)
        completion(model)
    } catch {
        log(error.localizedDescription, level: .error)
    }
}

So you can just call your function like:

decode(data, completion: { (user: User) in
            // Do something with your parsed user struct or whatever you wanna parse
        })

I hope this helps :D