Let's say that I have a model like the following, which allows me to build a tree of Foo objects.
struct Foo {
var kind : Kind
enum Kind {
case node([Foo])
case leaf
}
}
How can I make this Codable, specifically for the case node([Foo])
?
Here's the final struct, based on the answer from @PauloMattos:
Base Foo struct:
struct Foo {
var name: String
var kind: Kind
enum Kind {
case node([Foo])
case leaf
}
init(name: String, kind: Kind) {
self.name = name
self.kind = kind
}
}
Codable Protocol extension:
extension Foo : Codable {
enum CodingKeys: String, CodingKey {
case name
case nodes
}
enum CodableError: Error {
case decoding(String)
case encoding(String)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
switch kind {
case .node(let nodes):
var array = container.nestedUnkeyedContainer(forKey: .nodes)
try array.encode(contentsOf: nodes)
break
case .leaf:
break
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Assumes name exists for all objects
if let name = try? container.decode(String.self, forKey: .name) {
self.name = name
self.kind = .leaf
if let array = try? container.decode([Foo].self, forKey: .nodes) {
self.kind = .node(array)
}
return
}
throw CodableError.decoding("Decoding Error")
}
}
CustomStringConvertable Protocol extension (to output string from the tree):
extension Foo : CustomStringConvertible {
var description: String {
return stringDescription(self)
}
private func stringDescription(_ foo: Foo) -> String {
var string = ""
switch foo.kind {
case .leaf:
return foo.name
case .node(let nodes):
string += "\(foo.name): ("
for i in nodes.indices {
string += stringDescription(nodes[i])
// Comma seperate all but the last
if i < nodes.count - 1 { string += ", " }
}
string += ")"
}
return string
}
}
And example testing code:
let a = Foo(name: "A", kind: .leaf)
let b = Foo(name: "B", kind: .leaf)
let c = Foo(name: "C", kind: .leaf)
let d = Foo(name: "D", kind: .node([b, c]))
let root = Foo(name: "ROOT", kind: .node([a, d]))
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try! encoder.encode(root)
let json = String(data: jsonData, encoding: .utf8)!
print("Foo to JSON:")
print(json)
let decoder = JSONDecoder()
do {
let foo = try decoder.decode(Foo.self, from: jsonData)
print("JSON to Foo:")
print(foo)
} catch {
print(error)
}
Output:
Foo to JSON:
{
"name" : "ROOT",
"nodes" : [
{
"name" : "A"
},
{
"name" : "D",
"nodes" : [
{
"name" : "B"
},
{
"name" : "C"
}
]
}
]
}
JSON to Foo:
ROOT: (A, D: (B, C))
One possible encoding for the Foo
recursive data type could be:
struct Foo: Encodable {
var name: String // added a per-node payload as well.
var kind: Kind
enum Kind {
case node([Foo])
case leaf
}
enum CodingKeys: String, CodingKey {
case name
case nodes
}
func encode(to encoder: Encoder) throws {
var dict = encoder.container(keyedBy: CodingKeys.self)
try dict.encode(name, forKey: .name)
switch kind {
case .node(let nodes):
var array = dict.nestedUnkeyedContainer(forKey: .nodes)
try array.encode(contentsOf: nodes)
case .leaf:
break // Nothing to encode.
}
}
}
A simple test using the JSON encoder:
let a = Foo(name: "A", kind: .leaf)
let b = Foo(name: "C", kind: .leaf)
let c = Foo(name: "B", kind: .leaf)
let root = Foo(name: "ROOT", kind: .node([a, b, c]))
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try! encoder.encode(root)
let json = String(data: jsonData, encoding: .utf8)!
print(json)
would then output the following JSON:
{
"name" : "ROOT",
"nodes" : [
{
"name" : "A"
},
{
"name" : "C"
},
{
"name" : "B"
}
]
}
Conforming to Decodable
should follow a similar logic ;)
Here is a great post of Decoadable
protocol and its usage.
I think at the bottom of the post in the Enum section you can find what you need, but if you don't want to read the article here is the gist which can be helpful.