How to extract data from nested JSON with Swift 4

2020-03-07 04:31发布

问题:

I have a JSON data structure using unique keys that are created on upload. I can read all of it if I read each dictionary item line by line. However, I'm trying to modify my code to use the Swift 4 codable properties.

Doing the Ray Wenderlich tutorial and reading the Ultimate Guide to JSON Parsing with Swift unfortunately did not propel me to genius status.

The JSON looks like this simple example: NOTE that keys, like "123", "456", "case1", "case2", "u1", "u2" are not known at run time.

   let json = """
    {
    "things" : {
        "123" : {
        "name" : "Party",
        "owner" : "Bob",
        "isActive" : true,
        "cases" : {
            "case1" : {
                "no" : 1
            },
            "case2" : {
                "no" : 2
            }
        }
        },
        "456" : {
        "name" : "Bus",
        "owner" : "Joe",
        "isActive" : true
        }
    },
    "users" : {
        "u1" : {
        "name" : "Summer"
        },
        "u2" : {
        "name" : "Daffy"
        }
    }
    }
    """

Following this SO question on flattening JSON, I was able to create a decoder for most of my data, but not for the nested dictionaries (in the example, cases is acting like a nested dictionary). I am sure that I am missing something simple.

If I attempt to include the commented out portion, the playground will not run, no error is given.

    struct Thing: Decodable {
    let id: String
    let isActive: Bool
    let name: String
    let owner: String
    //var cases = [Case]()

    init(id: String, isActive: Bool, name: String, owner: String){//}, cases: [Case]?) {
        self.id = id
        self.isActive = isActive
        self.name = name
        self.owner = owner
        //self.cases = cases ?? [Case(id: "none", caseNumber: 0)]
    }
}
struct User: Decodable {
    let id: String
    let name: String
}
struct Case: Decodable {
    let id: String
    let caseNumber: Int
}
struct ResponseData: Decodable {
    var things = [Thing]()
    var users = [User]()


    enum CodingKeys: String, CodingKey {
        case trips
        case users
    }

    private struct PhantomKeys: CodingKey {
        var intValue: Int?
        var stringValue: String
        init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
        init?(stringValue: String) { self.stringValue = stringValue }
    }

    private enum ThingKeys: String, CodingKey {
        case isActive, name, owner, cases
    }
    private enum UserKeys: String, CodingKey {
        case name
    }
    private enum CaseKeys: String, CodingKey {
        case id
        case caseNumber = "no"
    }

    init(from decoder: Decoder) throws {

        let outer = try decoder.container(keyedBy: CodingKeys.self)
        let thingcontainer = try outer.nestedContainer(keyedBy: PhantomKeys.self, forKey: .things)

        for key in thingcontainer.allKeys {
            let aux = try thingcontainer.nestedContainer(keyedBy: ThingKeys.self, forKey: key)

            let name = try aux.decode(String.self, forKey: .name)
            let owner = try aux.decode(String.self, forKey: .owner)
            let isActive = try aux.decode(Bool.self, forKey: .isActive)
//            let c = try aux.nestedContainer(keyedBy: CaseKeys.self, forKey: .cases)
//            var cases = [Case]()
//            for ckey in c.allKeys {
//                let caseNumber = try c.decode(Int.self, forKey: .caseNumber)
//                let thiscase = Case(id: ckey.stringValue, caseNumber: caseNumber)
//                cases.append(thiscase)
//            }

            let thing = Thing(id: key.stringValue, isActive: isActive, name: name, owner: owner)//, cases: cases)
            things.append(thing)
        }
        let usercontainer = try outer.nestedContainer(keyedBy: PhantomKeys.self, forKey: .users)

        for key in usercontainer.allKeys {
            let aux = try usercontainer.nestedContainer(keyedBy: UserKeys.self, forKey: key)
            let name = try aux.decode(String.self, forKey: .name)
            let user = User(id: key.stringValue,name: name)
            users.append(user)
        }
    }
}

It works for the things and users, but I have to ignore the cases. See the output of print in comments//.

let data = json.data(using: .utf8)!
let things = try JSONDecoder().decode(ResponseData.self, from: data).things
print(things[0])
//Thing(id: "456", isActive: true, name: "Bus", owner: "Joe")

let users = try JSONDecoder().decode(ResponseData.self, from: data).users
print(users[0])
//User(id: "u1", name: "Summer")

I have tried to use the guidance from this SO question on decoding that seems much cleaner to me, but I have not successfully implemented it.

This code is also a GIST

My question is twofold:

  1. How can I get the Case data as a nested array in my Thing?
  2. Can you suggest a cleaner/shorter way to code this? It feels like I'm repeating things, but I have seen this kind of wrapper structure in several examples for JSON encoding/decoding.

回答1:

You can try something like this:

let data = jsonData.data(using: .utf8)
let json = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
let things = json["things"] as! [String:Any]

for (thing_key, thing_value)   in things as [String:Any] {

    let thing = thing_value as! [String:Any]
    if let cases = thing["cases"] as? [String:Any]{
        for (case_key, case_value) in cases {
            print(case_key)
            print(case_value)
        }
    }
}

EDIT I initially missunderstood your question , here is your code improved to obtain the cases. It was a quick job so might not be optimal, but you get the idea:

struct Thing: Decodable {
    let id: String
    let isActive: Bool
    let name: String
    let owner: String
    var cases: [Case]?

    init(id: String, isActive: Bool, name: String, owner: String , cases: [Case]?) {
        self.id = id
        self.isActive = isActive
        self.name = name
        self.owner = owner
        self.cases = cases

    }
}
struct User: Decodable {
    let id: String
    let name: String
}
struct Case: Decodable {
    let id: String
    let caseNumber: Int
}
struct ResponseData: Decodable {
    var things = [Thing]()
    var users = [User]()


    enum CodingKeys: String, CodingKey {
        case things
        case users
        case cases
    }

    private struct PhantomKeys: CodingKey {
        var intValue: Int?
        var stringValue: String
        init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
        init?(stringValue: String) { self.stringValue = stringValue }
    }

    private enum ThingKeys: String, CodingKey {
        case isActive, name, owner, cases
    }
    private enum UserKeys: String, CodingKey {
        case name
    }
    private enum CaseKeys: String, CodingKey {

        case no
    }

    init(from decoder: Decoder) throws {

        let outer = try decoder.container(keyedBy: CodingKeys.self)
        let thingcontainer = try outer.nestedContainer(keyedBy: PhantomKeys.self, forKey: .things)

        for key in thingcontainer.allKeys {
            let aux = try thingcontainer.nestedContainer(keyedBy: ThingKeys.self, forKey: key)

            let name = try aux.decode(String.self, forKey: .name)
            let owner = try aux.decode(String.self, forKey: .owner)
            let isActive = try aux.decode(Bool.self, forKey: .isActive)


            var cases:[Case]? = []
            do{
                let casescontainer = try aux.nestedContainer(keyedBy: PhantomKeys.self, forKey: .cases)
                    for case_key in casescontainer.allKeys{

                        let caseaux = try casescontainer.nestedContainer(keyedBy: CaseKeys.self, forKey: case_key)
                        let no = try caseaux.decode(Int.self, forKey: .no)
                        let thingCase = Case(id:case_key.stringValue, caseNumber: no)
                        cases?.append(thingCase)
                }

            }catch{ }


            let thing = Thing(id: key.stringValue, isActive: isActive, name: name, owner: owner , cases: cases)
            things.append(thing)
        }
        let usercontainer = try outer.nestedContainer(keyedBy: PhantomKeys.self, forKey: .users)

        for key in usercontainer.allKeys {
            let aux = try usercontainer.nestedContainer(keyedBy: UserKeys.self, forKey: key)
            let name = try aux.decode(String.self, forKey: .name)
            let user = User(id: key.stringValue,name: name)
            users.append(user)
        }
    }
}

This produce this output:

let data = json.data(using: .utf8)!
let things = try JSONDecoder().decode(ResponseData.self, from: data).things
print("-----")
for thing in things{
    print(thing)
}
print("---")
let users = try JSONDecoder().decode(ResponseData.self, from: data).users
for user in users{
    print(user)
}

-----
Thing(id: "456", isActive: true, name: "Bus", owner: "Joe", cases: Optional([]))
Thing(id: "123", isActive: true, name: "Party", owner: "Bob", cases: Optional([__lldb_expr_283.Case(id: "case1", caseNumber: 1), __lldb_expr_283.Case(id: "case2", caseNumber: 2)]))
---
User(id: "u1", name: "Summer")
User(id: "u2", name: "Daffy")


回答2:

You can get keys from your current json as

 jq -r 'keys[]'

After that query in the loop by each key retrieved



标签: json swift4