1

I am using reflection in Swift to write a generic object (Any) to dictionary in Swift; both for educational and production purposes.

The code works but one of my tests showed that for self-referential classes, it does not work properly.

When accessing the referenced member field whose type is same as the class, the Mirror.child.label field returns a value of some as the dictionary key , before accessing the member's variables.

A good example:

class Node{
  var next: Node!
  var val: Int = 0

init(_ val: Int){
self.val = val
}

}
  
 
let node = Node(4)
node.next = Node(5)
node.next.next = Node(6)


print("node.toDictionary: " , toDictionary(obj: node))

The output is:

node.toDictionary:  ["val": 4, "next": ["some": ["next": ["some": ["val": 6, "next": [:]]], "val": 5]]]

Needless to say, the expected output is:

["next": ["next": ["val": 6],"val": 5],"val": 4]

The minimal code for toDictionary which produces this error is:

func toDictionary(obj: Any) -> [String:Any] {
    var dict = [String:Any]()
    let otherObj = Mirror(reflecting: obj)
    for child in otherObj.children {
        if let key = child.label {
          
            if child.value is String || child.value is Character || child.value is Bool
                || child.value is Int  || child.value is Int8  || child.value is Int16  || child.value is Int32 || child.value is Int64
                || child.value is UInt  || child.value is UInt8  || child.value is UInt16  || child.value is UInt32 || child.value is UInt64
                || child.value is Float  || child.value is Float32 || child.value is Float64 || child.value is Double
            {
                dict[key] = child.value
            }
            else if child.value is [String] || child.value is [Character] || child.value is [Bool]
                || child.value is [Int]  || child.value is [Int8]  || child.value is [Int16]  || child.value is [Int32] || child.value is [Int64]
                || child.value is [UInt]  || child.value is [UInt8]  || child.value is [UInt16]  || child.value is [UInt32] || child.value is [UInt64]
                || child.value is [Float]  || child.value is [Float32] || child.value is [Float64] || child.value is [Double]{
                
                let array = child.value as! [Any]
                
                
                dict[key] = array
                
                
            }else if child.value is [Any]{
                 let array = child.value as! [Any]
                 var arr:[Any] = []
                for any in array{
                    arr.append(toDictionary(obj: any))
                }
                dict[key] = arr
            }
            else{
                dict[key] = toDictionary(obj: child.value)
            }
            
        }
    }
    return dict
}

Why is the Mirror.child.label field not handling self-referential classes properly?

gbenroscience
  • 994
  • 2
  • 10
  • 33
  • 2
    That’s because `next` is an optional, not because the class is self-referential. – Martin R May 08 '21 at 07:25
  • Thanks a lot @MartinR. I just tested it and you are correct. Would you mind posting an answer so I can accept? Also, will json decoders decode this kind of dictionary correctly back into the object? – gbenroscience May 08 '21 at 08:15

1 Answers1

1

The reason is that var: next: Node! is an implicitly unwrapped optional, and those are in many cases treated like regular optionals, see for example

So the value of node.next is actually Optional<Node>.some(nextNode), and that causes the ["some": ...] in the created dictionary.

The effect is unrelated to the property being self-referential, as the following example demonstrates:

class Bar {
    let value = 1
}

class Foo {
    var bar: Bar! = Bar()
}

let foo = Foo()
print(toDictionary(obj: foo))
// ["bar": ["some": ["value": 1]]]
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382