Binding JSON to Data Objects using KVC

Working with JSON data in iOS is pretty straightforward. For example, suppose you're writing an application that retrieves some simple statistical information from a web service:

{"count": 3, "sum": 9.0, "average": 3.0}

You might load the data from the server, call the JSONObjectWithData:options:error: method of the NSJSONSerialization class, and retrieve the values from the deserialized dictionary as follows:

NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

NSLog(@"%d", [[dictionary objectForKey:@"count"] intValue]); // prints 3
NSLog(@"%.1f", [[dictionary objectForKey:@"sum"] doubleValue]); // prints 9.0
NSLog(@"%.1f", [[dictionary objectForKey:@"average"] doubleValue]); // prints 3.0

However, sometimes it is preferable to work with more strongly typed data objects in an application. For example, you might create a Statistics class to represent the data returned by the web service:

@interface Statistics : NSObject

@property (nonatomic) int count;
@property (nonatomic) double sum;
@property (nonatomic) double average;

@end

You could then populate your object by extracting the values from the dictionary as follows:

Statistics *statistics = [[Statistics alloc] init];

statistics.count = [[dictionary objectForKey:@"count"] intValue];
statistics.sum = [[dictionary objectForKey:@"sum"] doubleValue];
statistics.average = [[dictionary objectForKey:@"average"] doubleValue];

To make things easier and avoid code duplication, you might put this code in an initializer for the Statistics class:

- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
    self = [super init];

    if (self) {
        self.count = [[dictionary objectForKey:@"count"] intValue];
        self.sum = [[dictionary objectForKey:@"sum"] doubleValue];
        self.average = [[dictionary objectForKey:@"average"] doubleValue];
    }

    return self;
}

Your code for binding the JSON response to the Statistics instance would then be reduced to this:

Statistics *statistics = [[Statistics alloc] initWithDictionary:dictionary];

In either case, you could then access the data returned from the server using the strongly typed properties of your data object:

NSLog(@"%d", statistics.count); // prints 3
NSLog(@"%.1f", statistics.sum); // prints 9.0
NSLog(@"%.1f", statistics.average); // prints 3.0

This works reasonably well, and it is undoubtedly a common approach to mapping JSON content to strongly typed data objects.

However, there is an even simpler solution: key-value coding (KVC). The setValuesForKeysWithDictionary: method of NSObject can be used to automatically apply all values in a given dictionary to an object's properties. Using this approach, the initWithDictionary: method can be reduced to the following:

- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
    self = [super init];

    if (self) {
        [self setValuesForKeysWithDictionary:dictionary];
    }

    return self;
}

No manual mapping of dictionary entries to property values is required. Simply declaring the properties using the appropriate name and type is sufficent. It also works in Swift:

class Statistics: NSObject {
    var count: Int = 0
    var sum: Double = 0
    var average: Double = 0

    init(dictionary: [String: AnyObject]) {
        super.init()

        setValuesForKeys(dictionary);
    }
}

Further, if you need to customize the assignment of any property names or values, you can do so easily by overriding setValue:forKey:. For example, suppose the server refers to the average property by a different name (e.g. "mean"):

{"count": 3, "sum": 9.0, "mean": 3.0}

You could override setValue:forKey: to ensure that this value is mapped to the correct property:

- (void)setValue:(id)value forKey:(NSString *)key {
    if ([key isEqual:@"mean"]) {
        key = @"average";
    }

    [super setValue:value forKey:key];
}

Finally, you can use KVC to easily ignore values. For example, suppose the server response additionally contains a "median" property:

{"count": 3, "sum": 9.0, "average": 3.0, "median": 3.0}

Since the Statistics class does not define a median property, setValuesForKeysWithDictionary: will throw an NSUnknownKeyException. You can avoid this exception by simply overriding the setValue:forUndefinedKey: method:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    // No-op
}

For more ways to simplify iOS app development, please see my projects on GitHub:

  • MarkupKit – Declarative UI for iOS and tvOS
  • HTTP-RPC – Lightweight multi-platform REST

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s