Scripting in Swift: Any perils

Swift does a lot in the way of type-checking to try to prevent your app from crashing and burning, but there are times when it’s up to the programmer. One of those is when handling a result which can be of any type: an Any.

When I was working on Consolation 3, I had inconveniently forgotten the perils of Any, and doing so resulted in a crashing bug, for which I am greatly embarrassed. The problem lay behind a single line of code
let json3 = json2?.object(forKey: theKeys[theIndex])

To enable the new styles and search features in Consolation 3, the app is working with JSON format log extracts. These are read in using a powerful JSONSerialization feature:
let json = try? JSONSerialization.jsonObject(with: theJSONdata, options: [])
takes the JSON-format text generated by the log show command and transforms it into an array of dictionaries, each of which represents one log entry.

My code then steps through these dictionaries, which consist of key-value pairs representing each field in the entry. What I had forgotten is that some of those values are not strings, but integers. To fetch the value for a given key in that dictionary, I call
let json3 = json2?.object(forKey: theKeys[theIndex])
where json2 is the individual dictionary, and theKeys[theIndex] resolves to a key, such as "timestamp". But this call doesn’t return a string in json3, it returns an Any. When the value is a string, as is the case for most of the fields here, json3 will be a string, and I can append it to the growing string which will form the log extract.

It hadn’t occurred to me – although I should have known – that for some keys, the value is not a string, but an integer. So the first time that my style formatter hits one of those integer values, there’s a runtime error which cannot be handled. That is expressed as a crash or an ‘unexpected quit’.

What I thus needed to do was to determine what type the value had, and if an integer convert it to a string. I thought that I would try this out in an Xcode Playground, but that only made things worse.

In a Swift Playground, I found that I could bludgeon my way through:
returns a string, whether you pass it a string or an integer. Unfortunately, outside the playground, real-world Swift isn’t so accommodating. Because the type is marked as an optional, and that form of String initialisation has been replaced with String(describing:), I would have to use
String(describing: anything)
which results in strings like “optional(325)”. The same applies to tricks like "\(anything)".

The only sensible way around this, as far as I can see, is to test whether you’re working with a string or integer value, and handle the two types appropriately. The best that I could come up with is:
if (json3 is String) {
json4 = json3 as! String
} else {
json4 = "\(json3 ?? 0)"

This first checks to see if the value is a String type. If it is, then it is handled as a string. If it isn’t, the way round the optional problem is to supply a default value of 0, and format the integer into a string. As with most things in Swift, there are several other solutions, but I think that is simple and fairly efficient. Most importantly, it also stops the crashing.