Search code examples
jsonswiftjsonparser

JSON parsing in swift. What's the best structure parse a json file


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?


Solution

  • (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.