Search code examples
iosjsonswiftalgorithmrecursive-datastructures

How to see all "ancestors" of a recursive data model?


I am receiving a JSON:

{
    "categories":
    [
        {
            "category_name": "example name",
            "children":
            [
                {
                    "category_name": "example name"
                },
                {
             ...

As can be seen, the data is a recursive format. I was able to write the code for decoding it into my custom type which is:

 struct Name: Codable {
        let cat: String
        let children: [cat]?
}

Now, for any cat, I would like to know the "path" of it. As in, I'd like know what are all the super(ancestor) categories. So, for the category "tablets", I would like to be able to traverse what the drill down structure looks like, which in this case could look like:

Electronics -> Computers -> Laptops and Tablets -> Tablets

How do I structure my code or data model to be able to retrieve this information for any category?


Solution

  • First, you'll want to add the path to Category so you have somewhere to store the data. For convenience, I'll also add a CategoriesResponse just to handle the top-level structure, but it's not really important:

    struct CategoriesResponse: Decodable {
        var categories: [Category]
    }
    
    struct Category {
        let path: [String]
        let categoryName: String
        let children: [Category]
    }
    

    (I'm assuming what you want are just the names of the parent categories. If you want references of some kind, that's possible, but the data structures get a little more complicated. This basic approach will still work, though. Let me know if you need something like that, and I can expand the answer.)

    And of course standard CodingKeys stuff:

    private enum CodingKeys: String, CodingKey {
        case categoryName = "category_name"
        case children
    }
    

    The meat of the solution is that you need an init that can accept a KeyedDecodingContainer (rather than a Decoder) and a path, and handle decoding everything else.

    // For each element, decode out of the container by hand rather than recursing into init(from: Decoder)
    private init(from container: KeyedDecodingContainer<CodingKeys>, path: [String]) throws {
        // Track our own path up to this point
        self.path = path
    
        // Unload the usual stuff
        self.categoryName = try container.decode(String.self, forKey: .categoryName)
    
        // Construct the children, one element at a time (if children exists)
        var children: [Category] = []
    
        if container.contains(.children) {
            // Extract the array of children
            var childrenContainer = try container.nestedUnkeyedContainer(forKey: .children)
            while !childrenContainer.isAtEnd {
                // Extract the child object
                let childContainer = try childrenContainer.nestedContainer(keyedBy: CodingKeys.self)
    
                // For each child, extend the path, decode
                let child = try Category(from: childContainer, path: path + [self.categoryName])
    
                // And append
                children.append(child)
            }
        }
        self.children = children
    }
    

    And finally, you need a Decodable implementation just to kick it all off:

    extension Category: Decodable {
        // Top level decoder to kick everything off
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            try self.init(from: container, path: [])
        }
    }
    

    With that, it should work as expected using a standard Decoder:

    let categories = try JSONDecoder().decode(CategoriesResponse.self, from: json).categories