Behind the scenes of product and engineering at Quri.

Better iOS/Mac Models Part 1

One thing that nearly every app that communicates with external services does is parse JSON and convert it into usable model objects. I’ve seen this handled numerous ways, the most common being some sort of for loop on an NSDictionary’s keys, looking for specific strings.

1
2
3
4
5
for (NSString *key in dictionary) {
  if ([key isEqualToString:kSomeJsonKey]) {
      self.someProperty = dictionary[key];
  } else if([key isEqualToString:kSomeOtherJsonKey]) . . . and so on
}

Not only is this a major pain to read, but it makes updating models tedious and error prone. After spending years populating my models this way, I set out to find something better. In post one of this three-post series I’ll introduce the mechanisms we use to automatically marshal JSON API responses to models. In part two I’ll introduce additional automatic NSCoding and NSCopying support and additional utility methods. Part three is the ultimate open sourcing of our base model class.

Core Functionality / Requirements

Before jumping into the code I will explain some of the requirements we have for our model objects. First, assume that all of our models will inherit from a single base class that handles most of the functionality.

1) Automatic marshaling of model(s) from an NSDictionary or NSArray representation

Passing an NSDictionary or NSArray to a single method will automatically generate a complete object graph based, complete with nested models and collections of models.

2) Automatic NSCoding & NSCopying support

All models, by default, support NSCoding & NSCopying so the app can easily persist them to disk and make deep copies. Very little, if any, scaffolding code should be required to support this.

3) Automatic conversion of NSString’s to types like NSDate, NSURL, etc.

Models often times contain more complex types, like NSDates, NSURLs and even things like (UI|NS)Images. Conversion to these types needs to be easy to setup and seamless.

ESModel

Now we’ll jump right into the base model class (paired down for part one) with the majority of our functionality and an example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#import <Foundation/Foundation.h>

typedef id (^ValueConverter)(id obj);

@interface ESModel : NSObject <NSCopying, NSCoding>

+ (NSDictionary *)valueMap;

+ (instancetype)fromDictionary:(NSDictionary *)d;

+ (NSMutableArray *)fromArray:(NSArray *)a;

+ (NSDictionary *)valueConverters;

+ (ValueConverter)modelValueConverter;

+ (ValueConverter)arrayOfModelsValueConverter;

@implementation ESModel

+ (NSDictionary *)valueMap {
    @throw [NSException exceptionWithName:@"Abstract Method Access" reason:@"This method needs to be overridden by any inheriting class." userInfo:nil];
}

+ (instancetype)fromDictionary:(NSDictionary *)d {
    id model = [[[self class] alloc] init];

    NSDictionary *valueMap = [self valueMap];
    NSDictionary *valueConverters = [self valueConverters];

    for (NSString *key in valueMap) {
        id value = d[key];
        if (value) {

            // skip NSNulls
            if (value == [NSNull null]) continue;

            // convert if there is a converter
            if (valueConverters[key]) {
                ValueConverter valueConverter = valueConverters[key];
                value = valueConverter(value);
            }

            // finally set the value
            [model setValue:value forKey:valueMap[key]];
        }
    }

    return model;
}

+ (NSMutableArray *)fromArray:(NSArray *)a {
    NSMutableArray *models = [NSMutableArray arrayWithCapacity:[a count]];
    for (NSDictionary *d in a) {
        id obj = [[self class] fromDictionary:d];
        [models addObject:obj];
    }

    return models;
}

+ (NSDictionary *)valueConverters {
    return nil;
}

+ (ValueConverter)modelValueConverter {
    return [^id (id obj) {
        NSDictionary *d = (NSDictionary *)obj;
        return [[self class] fromDictionary:d];
    } copy];
}

+ (ValueConverter)arrayOfModelsValueConverter {
    return [^id (id obj) {
        NSArray *a = (NSArray *)obj;
        NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:[a count]];

        for (NSDictionary *d in a) {
            [newArray addObject:[[self class] fromDictionary:d]];
        }

        return newArray;
    } copy];
}


/**

Ex json:

{
  "key_1":"foo",
  "key_2":"bar",
  "model_2": { "key_3":"bat" },
  "array_of_model_2s": [ { "key_3":"something" }, { "key_3":"else" } ]
}

*/

@interface MyModel : ESModel
  
@property (nonatomic, strong) NSString *modelProperty1;
@property (nonatomic, strong) NSString *modelProperty2;
@property (nonatomic, strong) ModelTwo *model2; 
@property (nonatomic, strong) NSArray *model2s;

@end

@implementation MyModel
  
+ (NSDictionary *)valueMap {
  return @{    
      @"key_1":@"_modelProperty1",
      @"key_2":@"_modelProperty2",
      @"model_2":@"_model2",
      @"array_of_model_2s":@"_model2s"
  }
}
  
+ (NSDictionary *)valueConverters {
  return @{
      @"model_2":[ModelTwo valueConverter],
      @"array_of_model_2s":[ModelTwo arrayOfModelsValueConverter]
  }
}
  
@end   

Woah, that’s a lot to digest. Let’s go through it and use MyModel to illustrate ESModel works.

First up is + (NSDictionary *)valueMap;. This method must be implemented by any sub-classes and is the basis for the conversion of NSDictionary key-value pairs to model properties. If you look at MyModel you can see it simply returns a dictionary with the keys corresponding to what would typically be JSON keys and the values corresponding to the model’s ivars.

The next method is a biggie and provides the main functionality we desire. As its name implies, fromDictionary handles converting a dictionary into a model class. Let’s go through it in chunks so it’s easier to digest.

1
2
3
4
5
+ (instancetype)fromDictionary:(NSDictionary *)d {
    id model = [[[self class] alloc] init];

    NSDictionary *valueMap = [self valueMap];
    NSDictionary *valueConverters = [self valueConverters];

These lines are rather self explanatory. Since they’re executed in the context of a sub-class of ESModel they’re initalizing an instance of MyModel and pulling out the valueMap and valueConverters. The valueConverters method provides functionality to marshall any obj-c class from some NSDictionary representation (and by extension JSON representation). More on this later.

1
2
3
4
5
6
for (NSString *key in valueMap) {
        id value = d[key];
        if (value) {

            // skip NSNulls
            if (value == [NSNull null]) continue;

Next start looping through the dictionary representation and attempt to pull out values based on the keys of our valueMap. So in the case of our JSON we’ll get two NSStrings, one NSDictionary (model_2) and one NSArray (array_of_model_2s). I’ve never found [NSNull null] particularly useful and prefer the behavior of nil so we skip over those values. That said, your mileage may very and adding a flag to conditionally skip nulls is easy enough.

1
2
3
4
5
        // convert if there is a converter
        if (valueConverters[key]) {
            ValueConverter valueConverter = valueConverters[key];
            value = valueConverter(value);
        }

Next we check for a ValueConverter for this key. A ValueConverter is a very simple block that converts one object to another. The blocks signature is:

1
typedef id (^ValueConverter)(id obj);

In ESModel you’ll notice there is a method named valueConverters. This optional, overridable method returns a NSDictionary with all of the keys that should be converted from one representation to another. Looking at MyModel you can see it returns two keys: one for a single model and one for an array of models. These methods are convinentaly implemented for any model that inherits from ESModel. More on those in a second.

Finally, wrapping up fromDictionary we use KVC to set the property on our model based on the value from valueMap:

1
2
3
4
5
6
7
        // finally set the value
        [model setValue:value forKey:valueMap[key]];
    }
}

return model;
}

Next, there is a very simple method named fromArray that converts an array of dictionaries into models…as you’d expect it simply loops over the array and calls fromDictionary on itself.

The more interesting bits come after, the ValueConverter methods we touched on earlier. Let’s look at them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (ValueConverter)modelValueConverter {
    return [^id (id obj) {
        NSDictionary *d = (NSDictionary *)obj;
        return [[self class] fromDictionary:d];
    } copy];
}

+ (ValueConverter)arrayOfModelsValueConverter {
    return [^id (id obj) {
        NSArray *a = (NSArray *)obj;
        NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:[a count]];

        for (NSDictionary *d in a) {
            [newArray addObject:[[self class] fromDictionary:d]];
        }

        return newArray;
    } copy];
}

The first ValueConverter method is a straight forward utility method that utilizes fromDictionary. It lets us compose complex model graphs in our JSON responses and have everything convert to the proper models. MyModel uses this to convert another embedded model in its JSON to the correct class. The second method is similar to the first, converting an array of embedded models into the correct representation.

What’s Next

In part two we’ll cover the following:

  • Additional ValueConverter topics
  • Refactoring ESModel to automatically handle NSCoding and NSCopying

Stay tuned and feel free to send any feedback or questions to brian@quri.com. Thanks!

Comments