-1

I am trying to parse a json file that looks like this:

{
"MyApp": {
    "pro1" : {
        "enable": true
    },
    "pro2" : {
        "enable": true
    }
},
"pro3" : {
    "pro4" : true,
    "pro5" : true,
    "pro6" : true,
    "pro7" : true,
    "pro8" : true,
    "pro10" : true
},
"pro11": {
    "pro12": false,
    "pro13": false,
    "pro14": false
},
"pro15": {
    "prob16": true
},
"prob16": {
    "prob17": {
        "prob18": true,
    }
},
"prob19": {
    "prob20": {
        "prob21": {
            "prob22": false
        }
    }
},
"prob23": true,
"prob24": true
}

I am trying to parse it in a way that provides easy access. I first parsed the json file into a json object with type [String:Any], then I tried to put the pairs into [String:[String:[String:Bool]]] but then I realize a problem is that I don't know how many layers might be there. Maybe there will be pairs within pairs within pairs..

But If do know the layers, say the maximum layer is 4, do I still put this as a map? map within 3 other maps? Is there any better data structure to put this into?

Anna
  • 443
  • 9
  • 29
  • 1
    Codable nested structs. – matt Mar 13 '19 at 20:51
  • @matt what does that mean... – Anna Mar 13 '19 at 20:53
  • 1
    Also, JSON whose structure is not known in advance is bad JSON. What is the real structure here? – matt Mar 13 '19 at 20:54
  • @matt I am being serious, I don't know how many layers there might be. – Anna Mar 13 '19 at 20:54
  • 2
    You “a json file that looks like this”. I’m betting it doesn’t. Show the real JSON please. – matt Mar 13 '19 at 20:55
  • Then it can’t be parsed. There must be some rule governing the structure. Find out what that rule is. – matt Mar 13 '19 at 20:56
  • @matt Okay..but If I do know what the max layers can be, do I still parse it into a map? BTW this is the actual json file, I just changes the key and values to prob123 etc. – Anna Mar 13 '19 at 20:58
  • Don’t change them. Show the real JSON. – matt Mar 13 '19 at 20:59
  • @matt I can't. But the structure is the same tho, didn't change that. – Anna Mar 13 '19 at 21:00
  • 1
    Well that makes it hard to help. – matt Mar 13 '19 at 21:02
  • 1
    What do these keys and values mean? What kind of algorithms do you plan to write to consume this data? As written, this reads like gibberish, so there's no way to say how to parse it. How does "pro15" relate to "prob16?" Is the fact that some thing are "pro" and some things "prob" meaningful or random? When we see "prob16" two places in the JSON, are those the same string in the "real" JSON? We need some specific thing to parse such that when you have an answer that parses it, you would say "yes, that's what I wanted" rather than "well, that's not what I really mean." – Rob Napier Mar 13 '19 at 21:09
  • @RobNapier values are just boolean, each key is different and they are not related. And just tbh, the real json file is even harder to read because the keys make less sense than prob123. – Anna Mar 13 '19 at 21:13
  • So this is really a series of keypaths that terminate in a boolean, right? Do you know the keypaths you want to access, or do you actually need to navigate the tree to discover all the keypaths? What would your ideal *calling* call look like. As you've described it here in the comments, it sounds like you might actually just want a `[String: Bool]` that you access as `data["path.to.variable.im.interested.in"]`. Very very different than a `[String: Any]`. – Rob Napier Mar 13 '19 at 21:14
  • Similarly, do we know that every level is either a boolean or a new dictionary? Sounds like we do know that. That again, is very critical structure that we can recreate in our types and make interacting with this much much easier. But we need to know the goal. (Feels like a config file; and that's definitely something that can be attacked.) – Rob Napier Mar 13 '19 at 21:18
  • @RobNapier Every level is either a new dict or a boolean. I just need to put this json into a table view that's why I need to access every single pair. – Anna Mar 13 '19 at 21:19
  • That's definitely a use case with enough structure around it to solve. Give me a few minutes. – Rob Napier Mar 13 '19 at 21:20
  • @RobNapier Thanks I am literally pulling my brains out trying to parse it. – Anna Mar 13 '19 at 21:25

2 Answers2

1

(This is a partial answer, I suspect you will immediately have more questions, but until I know how you're going to use this data structure, I didn't want to write the helpers you'll need.)

As you say, each stage of this is either a boolean value, or another layer mapping strings to more stages. So say that in a type. When you describe something using the word or, that generally tells you it's an enum.

// Each level of Settings is either a value (bool) or more settings.
enum Settings {
    // Note that this is not order-preserving; it's possible to fix that if needed
    indirect case settings([String: Settings])
    case value(Bool)
}

You don't know the keys, so you need "any key," which is something that probably should be in stdlib, but it's easy to write.

// A CodingKey that handle any string
struct AnyStringKey: CodingKey {
    var stringValue: String
    init?(stringValue: String) { self.stringValue = stringValue }
    var intValue: Int?
    init?(intValue: Int) { return nil }
}

With those, decoding is just recursively walking the tree, and decoding either a level or a value.

extension Settings: Decodable {
    init(from decoder: Decoder) throws {
        // Try to treat this as a settings dictionary
        if let container = try? decoder.container(keyedBy: AnyStringKey.self) {
            // Turn all the keys in to key/settings pairs (recursively).
            let keyValues = try container.allKeys.map { key in
                (key.stringValue, try container.decode(Settings.self, forKey: key))
            }
            // Turn those into a dictionary (if dupes, keep the first)
            let level = Dictionary(keyValues, uniquingKeysWith: { first, _ in first })
            self = .settings(level)
        } else {
            // Otherwise, it had better be a boolen
            self = .value(try decoder.singleValueContainer().decode(Bool.self))
        }
    }
}

let result = try JSONDecoder().decode(Settings.self, from: json)

(How you access this conveniently depends a bit on what you want that table view to look like; what's in each row, what's your UITableViewDataSource look like? I'm happy to help through that if you'll explain in the question how you want to use this data.)

Swift Runner


The following code is probably way too complicated for you to really use, but I want to explore what kind of interface you're looking for. This data structure is quite complicated, and it's still very unclear to me how you want to consume it. It would help for you to write some code that uses this result, and then I can help write code that matches that calling code.

But one way you can think about this data structure is that it's a "dictionary" that can be indexed by a "path", which is a [String]. So one path is ["prob23"] and one path is ["prob19", "prob20", "prob21", "prob22"].

So to subscript into that, we could do this:

extension Settings {
    // This is generic so it can handle both [String] and Slice<[String]>
    // Some of this could be simplified by making a SettingsPath type.
    subscript<Path>(path: Path) -> Bool?
        where Path: Collection, Path.Element == String {
            switch self {
            case .value(let value):
                // If this is a value, and there's no more path, return the value
                return path.isEmpty ? value : nil

            case .settings(let settings):
                // If this is another layer of settings, recurse down one layer
                guard let key = path.first else { return nil }
                return settings[key]?[path.dropFirst()]

            }
    }
}

This isn't a real dictionary. It's not even a real Collection. It's just a data structure with subscript syntax. But with this, you can say:

result[["pro3", "pro4"]] // true

And, similarly, you get all the paths.

extension Settings {
    var paths: [[String]] {
        switch self {
        case .settings(let settings):

            // For each key, prepend it to all its children's keys until you get to a value
            let result: [[[String]]] = settings.map { kv in
                let key = kv.key
                let value = kv.value
                switch value {
                case .value:
                    return [[key]] // Base case
                case .settings:
                    return value.paths.map { [key] + $0 } // Recurse and add our key
                }
            }

            // The result of that is [[[String]]] because we looped twice over something
            // that was already an array. We want to flatten it back down one layer to [[String]]
            return Array(result.joined())
        case .value:
            return [] // This isn't the base case; this is just in case you call .paths on a value.
        }
    }
}

for path in result.paths {
    print("\(path): \(result[path]!)")
}

==>

["pro15", "prob16"]: true
["pro3", "pro4"]: true
["pro3", "pro10"]: true
["pro3", "pro7"]: true
["pro3", "pro8"]: true
["pro3", "pro5"]: true
["pro3", "pro6"]: true
["prob19", "prob20", "prob21", "prob22"]: false
["prob23"]: true
["prob24"]: true
["MyApp", "pro1", "enable"]: true
["MyApp", "pro2", "enable"]: true
["prob16", "prob17", "prob18"]: true
["pro11", "pro13"]: false
["pro11", "pro14"]: false
["pro11", "pro12"]: false

I know this is too complex an answer, but it may start getting you into the right way of thinking about the problem and what you want out of this data structure. Figure out your use case, and the rest will flow from that.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    Thanks a lot. I think the enum and recursion part makes sense. I'll implement this first thing tmr and tell you how it goes. But I think I get the idea. – Anna Mar 13 '19 at 23:00
  • Hey So I was able to implement this after some tries, now I am trying to access the data. I want to access in a way that's similar like maps, for each key I get the value, if the value is another settings I keep going until get a value. From my understanding, I add a getter in the Settings class? – Anna Mar 14 '19 at 19:36
  • Do you know how to use `switch` with enums? Or the `if case let` syntax to unload them? That's the primary tool you'll be using. – Rob Napier Mar 14 '19 at 19:41
  • I guess?I've not used enums this way before but I'll try. – Anna Mar 14 '19 at 19:46
  • After I pulled my and my friends' brains out, I did something like this. A function with switch statement that if the case is settings, recursively call itself until it reaches a case where it is a value. This would work but I need to manually track the layers and the parent keys, I am wondering is there any ways to access the data. – Anna Mar 14 '19 at 22:00
  • I noticed that the return value itself is a dictionary with string as key and settings as value, I am wondering is there anyway I can just access this dictionary? This way I don't need to keep track the layers and the parent. – Anna Mar 14 '19 at 22:09
  • When you say "access this dictionary," what syntax are you envisioning? What are you planning to *do* with the data, and in what form? The use case will drive what the structure looks like. You said you want to put this into a table view, but I'm still not clear what each row would look like; that's your starting point. What would you want this specific JSON input to look like on the screen? – Rob Napier Mar 14 '19 at 22:58
  • This is my picture, top layer key would be in a cell just by itself with a different colored background to indicate this is a different section/field. Other layer keys who has no value would just have an indentation in the cell to indicate that it belongs to be first layer however with no value. Keys with layer is going to have a like a toggle based on the boolean value. So it's more like pairs with a bool value is where the actual data is, the rest of the keys are like indicators to indicate which section it belongs to. – Anna Mar 15 '19 at 01:05
-2

In my opinion the best approach is to use AlamoFire, PromiseKit and SwiftyJSON those 3 Swift libraries will make parsing this complex JSON easier and in fewer lines and for sure it will save you lots of time, also they are highly documented.

Some sample code of how parsing with this libraries would work on the MyApp JSON field:

func DownloadDataAndParseJSON() {

 let headers = ["headerParameter": someParameter]

Alamofire.request(someUrl, method: .get, headers: headers).responseJSON { response in

    let json = JSON(response.result.value as Any)

    if let allJsonFields = json.array {

        for item in allJsonFields{

            let myApp = item["MyApp"].array ?? []

            for item in myApp{

                let pro1 = item["pro1"].array ?? []
                let pro2 = item["pro2"].array ?? []

                for item in pro1{
                    let enable = item["enable"].bool ?? false}

                for item in pro2{
                    let enable = item["enable"].bool ?? false}
            }
            //Here you can append data to dictionary, object or struct.
        }
      }
    }
  }

See how you can turn the parsed value to a boolean with .bool and also you can use the ?? parameter to add an optional default value in case the JSON throws a nil or empty field.

ikanimai
  • 47
  • 1
  • 8