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?
(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.)
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.