Sunday, 21 February 2016

From Objective-C to Swift: Approaches to decoding JSON

As developers, we must solve real world problems by using programming languages that impose on us various constraints. An algorithm therefore, might vary significantly depending on the characteristics of the implementation language. It's therefore important to think about how best to utilise the language features when reasoning about problems.
As an iOS developer, I've been affected by Apple's introduction of Swift. Swift's characteristics differ significantly from those of its predecessor, Objective-C. While it's usually possible to write Swift like objective-C' by making use of Objective-C's runtime, this doesn't mean that doing so provides the best approach. It's therefore important to think about how the problem can be solved in a 'Swifty' way. We can briefly summarise some of the differences between the languages:
  • - Objective-C dynamicness vs Swift's strongly-typed system
  • - Introspection is more extensive in Objective-C
  • - Swift has generics
  • - Objective-C allows sending messages to nil
  • - Swift's built-in vTable vs Objective-C's messages for method dispatch
To illustrate how the differences between these languages can affect the solution to the problem, we're going to look at a common task that exists on many iOS apps: mapping a JSON dictionary into a model object.
This is our very simple, example JSON file:
{
    "name" : "John Doe",
    "company" : "Doe Industries plc",
    "email" : "john@doeind.com",
    "partTime" : true
}
Our simple task is therefore to map this JSON file into an Address model object.
When approaching the problem as Objective-C developers, we might think that one good approach is to loop through the keys of the JSON dictionary, check if the model object has properties matching those keys and if so assign the appropiate value. We can implement this making use of Objective-C's powerful introspection:
Our Address class in Objective-C:
@interface Address : NSObject

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *company;
@property (copy, nonatomic) NSString *email;
@property (strong, nonatomic) NSNumber *partTime;

@end
Address *newObject = [[Address alloc] init];
    
NSString *jsonFilePath =
[[NSBundle mainBundle] pathForResource:@"address" ofType:@"json"];
NSData *data = [[NSFileManager defaultManager] contentsAtPath:jsonFilePath];
    
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
    
[json enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL* stop) {
        
    SEL selector = NSSelectorFromString(key);
    if ([newObject respondsToSelector:selector]) {
        [newObject setValue:value
                     forKey:key];
    }
}];
We could use the respondsToSelector: in Swift, however our object would have to inherit from NSObject and we would end up using the Objective-C runtime. This will not be possible if our model classes are Structs, or the properties are optionals for example. The same applies to the setValue:forKey: method as well as NSSelectorFromString().
We don't want to write Swift code as if it was Objective-C, constantly making use of the Objective-C runtime to achieve our goals. Nor do we want to avoid declaring our model objects as Structs. Even if we're happy to depend upon the Objective-C runtime, we wouldn't be able to declare the model object properties as let because we wouldn't be able to use setValue:forKey: with them. We therefore need to think about the problem differently, starting by the fact that Swift is a strongly-typed language in which methods are called not by method-dispatch at runtime, but by using a vTable.
While Swift has some introspection through the Mirror object, it's not enough to follow the same procedure, as we can't check if an object implements a certain method. In our example, we would need to know if the key of the parsed dictionary, has a corresponding property of the right type.
It's quite clear that it won't be possible to iterate through a dictionary of [String: AnyObject] and assign each key to an object property in Swift. We will need to actually list the keys we wish to map manually. This is not as bad as it sounds, given that we benefit from Swift's strong-type checks, reducing the likelihood of errors.
A simple Swift version could be:
struct Address {
    
    let name: String?
    let company: String?
    let email: String?
    let partTime: Bool?
    
    init(dict: [String: AnyObject]) {
        
        name = dict["name"] as? String
        company = dict["company"] as? String
        email = dict["email"] as? String
        partTime = dict["partTime"] as? Bool
    }
}
let path = NSBundle.mainBundle().pathForResource("address", ofType: "json")
let jsonData = NSData(contentsOfFile: path!)

do {
if let jsonDic = try NSJSONSerialization.JSONObjectWithData(jsonData!,
    options: .MutableContainers) as? [String: AnyObject] {
    
    let addr = Address(dict: jsonDic)
    // do something with your addr object
}
    
} catch {
    
}
Now that's an awful lot of casting, which is not only a pain to write but it also requires us to specify the right type otherwise the cast will fail and the property will be nil.
We can do better by using Swift's powerful Generics:
public func decode<T>(dictionary: [String: AnyObject], key: String) -> T? {
    return dictionary[key] as? T
}

struct Address {
    
    let name: String?
    let company: String?
    let email: String?
    let partTime: Bool?
    
    init(dict: [String: AnyObject]) {
        
        name = decode(dict, key: "name")
        company = decode(dict, key: "company")
        email = decode(dict, key: "email")
        partTime = decode(dict, key: "partTime")
    }
}
That's a significant improvement from having to type-cast every property. Naturally, it would be necessary to provide a decode function for non-optional types and for arrays to support embedded JSON objects.
If you're interested in learning more about this approach, have a look at Luciano's open-source JSON mapper called JSONUtilities. If you're interested in learning more about the Objective-C approach, you can have a look at my mapper TBRJSONMapper which is fully compatible with Swift and does not require you to write dictionary keys in your code.

Conclusion

In this post we've seen how the language features condition our approach to solving problems. We've looked at the problem of mapping a JSON file to a model object. Objective-C's dynamicness and introspection allows us to loop through the keys of the parsed dictionary and set the appropriate value into our model object if it implements the property for that key. This approach does not work in pure Swift (without using the Objective-C runtime), so instead we made use of Generics to map a dictionary entry into a model object property. The Swift approach requires us to specify the dictionary key in codes but we end up with a more robust, type-safe implementation.