Swift 4 parsing json with variable being the key [

2020-05-09 18:48发布

问题:

Im trying to get my head around the new Codable feature Apple has added but Im not able to solve this problem. I have a json output which is like this

{
    "Afpak": {
        "id": 1,
        "race": "hybrid",
        "flavors": [
            "Earthy",
            "Chemical",
            "Pine"
        ],
        "effects": {
            "positive": [
                "Relaxed",
                "Hungry",
                "Happy",
                "Sleepy"
            ],
            "negative": [
                "Dizzy"
            ],
            "medical": [
                "Depression",
                "Insomnia",
                "Pain",
                "Stress",
                "Lack of Appetite"
            ]
        }
    }
}

and I have a structure that is like

struct Strain: Codable {
    var name: String
    var id: Int
    var race: String
    var flavors: [String]
    var effects: [String: [String]]

}

so obviously it will fail because there is no name key inside my json. The name should be "Afpak", I have looked around but most tutorials didnt give any example on how to solve this problem, they just did [String:Strain] which is not what I need. Is there anyway set the key of my dictionary to my name variable?

回答1:

You can use a small trick to get around the unknown key issue: create a helper struct for your Strain struct, make that helper struct Codable, decode the response as [String:Helper], then create a custom initializer that takes 2 input arguments, the name of the strain and a Helper instance.

This way you can store the name as a property of Strain and you won't need an overcomplicated decoding to circumvent the issue of the unknown top level Dictionary key.

struct Strain {
    var name: String
    var id: Int
    var race: String
    var flavors: [String]
    var effects: [String: [String]]

    init(from helper: StrainHelper, with name:String){
        self.name = name
        self.id = helper.id
        self.race = helper.race
        self.flavors = helper.flavors
        self.effects = helper.effects
    }
}

struct StrainHelper: Codable {
    var id: Int
    var race: String
    var flavors: [String]
    var effects: [String: [String]]
}


do {
    let strainHelper = try JSONDecoder().decode([String:StrainHelper].self, from: data)
    guard let strainName = strainHelper.keys.first else {throw NSError(domain: "No key in dictionary",code: 404)}
    let strain = Strain(from: strainHelper[strainName]!, with: strainName)
} catch {
    print(error)
}


回答2:

Just make your name property optional, create a custom initializer to your structure that takes a dictionary and use its first key to initialise your name property:

enum ParseError: Error {
    case noKeyFound
}
struct Strain: Codable {
    let name: String!
    let id: Int
    let race: String
    let flavors: [String]
    let effects: [String: [String]]
    init(dictionary: [String: Strain]) throws {
        guard
            let key = dictionary.keys.first,
            let strain = dictionary[key] else { throw ParseError.noKeyFound }
        name = key
        id = strain.id
        race = strain.race
        flavors = strain.flavors
        effects = strain.effects
    }
}

do {
    let strain = try Strain(dictionary: JSONDecoder().decode([String:Strain].self, from: data))
    print(strain) // Strain(name: Afpak, id: 1, race: "hybrid", flavors: ["Earthy", "Chemical", "Pine"], effects: ["positive": ["Relaxed", "Hungry", "Happy", "Sleepy"], "negative": ["Dizzy"], "medical": ["Depression", "Insomnia", "Pain", "Stress", "Lack of Appetite"]])
} catch {
    print(error)
}