skip to Main Content

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?

3

Answers


  1. Here is a basic recursive solution that adds the path of elements to an array with the root element as the first and the targeted element as the last.

    It uses contains() so Category needs to conform to Equatable or it could be changed to use `contains(where:) instead like

    contains(where: { $0.categoryName == target.categoryName })
    

    if it is a more practical solution.

    func extractChain(for target: Category, parent: Category, chain: inout [Category]) {
        guard let children = parent.children else {
            chain = []
            return
        }
    
        chain.append(parent)
    
        if children.contains(target) {
            chain.append(target)
            return
        }
    
        for category in children where category.children != nil {
            extractChain(for: target, parent: category, chain: &chain)
            if !chain.isEmpty { return }
        }
        chain = [] // No match, clear the array
    }
    

    I’ve only made some basic tests, one with a match the 3rd level down and one with no match so some further testing is probably needed.

    Login or Signup to reply.
  2. You can use class and ancestor like this :

    class Category: Codable {
        let category_name: String
        let children: [Category]?
        var ancestor : Category?
    
        func setupAncestor(ancestor: Category?) {
            self.ancestor = ancestor
            if let children = children {
                for child in children {
                    child.setupAncestor(ancestor: self)
                }
            }
        }
    
        func ancestors() -> [Category] {
            if let ancestor = ancestor {
                var ancestorList = [ancestor]
                  ancestorList.append(contentsOf: ancestor.ancestors())
                 return ancestorList
            }
            return []
        }
        
        func ancestorsNames() -> [String] {
            if let ancestor = ancestor {
                var ancestorList = [ancestor.category_name]
                  ancestorList.append(contentsOf: ancestor.ancestorsNames())
                 return ancestorList
            }
            return []
        }
        
        func displayAncestors() {
            for ancestor in ancestors() {
                print("Ancestor : (ancestor.category_name)")
            }
        }
        func display(_ level: Int) {
            print("Level (level)")
            print("Category : (category_name) (ancestorsNames())")
            //displayAncestors()
            if let children = children {
                for child in children {
                    child.display(level+1)
                }
            }
        }
    }
    
    struct Categories : Codable {
        let categories: [Category]
        func setupAncestors() {
            for category in categories {
                category.setupAncestor(ancestor: nil)
            }
        }
    }
    
    let json = "..."
    let jsonData = json.data(using: .utf8)
    
    let decoder = JSONDecoder()
    
    do {
        let categories = try decoder.decode(Categories.self, from: jsonData!)
        categories.setupAncestors()
        for category in categories.categories {
            category.display(0)
        }
        
    } catch {
        print("Json decode error : (error)")
    }
    

    You can change the order and/or return only categoryName in the list

    EDIT : corrected the code

    Login or Signup to reply.
  3. 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
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search