0

This post is related to previous post I made. I wish to map the following nested dictionary:

["A": [["A1": ["A11", "A12"]], ["A2": ["A21", "A22"]]],
   "B": [["B1": ["B11", "B12"]], ["B2": ["B21", "B22"]]]
    ]

into a recursive struct:

Item(title:"",children:
    [Item(title:"A",children:
        [Item(title:"A1", children:
            [Item(title:"A11"),Item(title:"A12")]
            )]),
          Item(title:"B",children:
            [Item(title:"B1"),Item(title:"B2")]
        )]
)

with

struct Item: Identifiable {
    let id = UUID()
    var title: String
    var children: [Item] = []
}

To experiment, I started with ["A": [["A1": ["A11"]]], and made a json string:

let json1: String = """
            {"title": "", "children":[{"title": "A",
                                      "children": [{"title": "A1",
                                                    "children": [{"title": "A11"}]
                                                    }]
                                     }]
            }
"""



let decoder = JSONDecoder()
let info = try decoder.decode(Item.self, from: json.data(using: .utf8)!)
print(info)

It works only when I include "children": [] in the last node like this:

   let json2: String =  """
                {"title": "", "children":[{"title": "A",
                                          "children": [{"title": "A1",
                                                        "children": [{"title": "A11", "children": []}]
                                                    }]
                                     }]
            }
"""

What do I need to do to make json1 string work, so that even without the input of children, it will take the default value of []?

mic
  • 155
  • 2
  • 9

2 Answers2

1

You'll have to provide a custom init(decoder:) to handle that scenario. Handling this would require you to use the decodeIfPresent API of JSONDecoder's container and try to decode only if value is present and provide a default value if the condition fails. Here's how:

extension Item: Codable {
    enum CodingKeys: String, CodingKey {
        case title, children
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        children = try container.decodeIfPresent([Item].self, forKey: .children) ?? []
    }
}
Frankenstein
  • 15,732
  • 4
  • 22
  • 47
  • I just tried your solution and thanks, it works. I am new to swift and just want to know at which line does init(decoder: ) gets called? Is it in "let info = try decoder.decode(Item.self, from: json.data(using: .utf8)!)" ? how does it get called – mic Jun 11 '20 at 06:50
  • Yes, when you try to decode. The compiler calls this method to decode. – Frankenstein Jun 11 '20 at 07:56
0

One approach is to make children optional and use a computed property to access it. The computed property can return an empty array in the case where children is nil.

struct Item: Identifiable, Decodable {
    let id = UUID()
    var title: String
    private var privateChildren: [Item]?
    var children: [Item] {
        return self.privateChildren ?? []
    }

    enum CodingKeys: String,CodingKey {
        case title
        case privateChildren = "children"
    }
}

I have used a CodingKeys enumeration so that you can keep the same name in your JSON and in your code, but if you changed the property name in the JSON or in your code you could avoid that.

Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • I guess something is not working here, it shows: Item(id: DEB3941C-F13C-425A-A34E-C1382B545E12, title: "", privateChildren: Optional([__lldb_expr_171.Item(id: D83563AA-5AF9-4B52-8480-9742178DC3BD, title: "A", privateChildren: Optional([__lldb_expr_171.Item(id: B8163623-0A70-4C01-BA15-1148A42BA6FF, title: "A1", privateChildren: Optional([__lldb_expr_171.Item(id: 05D09104-50BC-4050-AE43-5BF1754B2A29, title: "A11", privateChildren: nil)]))]))])) – mic Jun 11 '20 at 07:35
  • I wish to have "children" as key, and the last privateChildren is nil but not [] – mic Jun 11 '20 at 07:37
  • `children` is a computed property so it won't show if you just `print(item)`, but it will have the value you want. try `print(item.children)` – Paulw11 Jun 11 '20 at 07:40
  • Ah, got it. Sorry it was a bad mistake. The last children is []: print(info.children[0].children[0].children[0].children) shows []. – mic Jun 11 '20 at 07:48