March 5, 2013 — Written with Nick Cipollina
Validation is always necessary when accepting input directly from users or remote servers, however it can quickly consume valuable development time to get it right. This post describes a pattern that builds on the foundations of Key-Value Coding (KVC) to automatically perform standard validation and also provides a framework for more complex processing. We will begin with a high-level overview of standard KVC and some common pitfalls, then describe our validation pattern and walk you through an example app demonstrating its benefits. If you'd like to skip right to the sample code, click here to jump to the Sample App section or click here for the source on GitHub.
KVC is a system of accessing and setting properties on an Objective-C object using the
string name of the property instead of calling the getter or setter method directly. If you
are new to KVC, the
Key-Value Coding Programming Guide
is an excellent overview of the concepts involved. The pattern is codified in the
NSKeyValueCoding
informal protocol, which includes methods like
valueForKey:
(a generic getter) and setValue:forKey
(a generic
setter). The two lines of code below that set the name property have the exact same effect:
Employee *somebody = [[Employee alloc] init]; // without KVC somebody.name = @"Bill"; // with KVC [somebody setValue:@"Bill" forKey:@"name"];Other methods accept a key path argument, which is a list of keys to traverse separated by a period. Key paths are a great way to save boilerplate code, for example to get the phone extension of Bill's manager:
// without KVC Employee *manager = bill.manager; PhoneNumber *phone = manager.phoneNumber; Extension *ext = phone.extension; // with KVC Extension *ext = [bill valueForKeyPath:@"manager.phoneNumber.extension"];The most important detail to note is that the key path is case-sensitive. Using
@"manager.phonenumber.extension"
would not work unless Employee had both
"phoneNumber" and "phonenumber" properties. Case sensitivity becomes important when you
encounter data that comes from an external source and could potentially change case at any time
(Pitfall #1).
One non-obvious KVC behavior is that calling setValue:forKey:
on an
NSArray
will not fail because arrays don't have keys, but instead will call
setValue:forKey:
on each object it contains.
KVC's most common use is when serializing or deserializing objects, a process that happens more
often than you might think. If your app stores object data on disk or transmits it across a
network, then you are most likely already using KVC under the hood. For example, Core Data
uses it to translate between its object representation and the underlying persistent store.
Other common uses are when parsing JSON web service results into an existing object or using
an NSKeyedArchiver
to serialize an object for storage or transmission. Typically
you create an initWithDictionary:
initializer to ingest dictionaries created by
NSJSONSerialization
or implement NSCoding
for more explicit
serialization support. The initWithDictionary:
method used in the sample app
simply turns around and uses KVC:
- (id)initWithDictionary:(NSDictionary *)dictionary { self = [self init]; if (self){ [self setValuesForKeysWithDictionary:dictionary]; } return self; }The one caveat to this is that you should implement
setValue:forUndefinedKey:
to
cover the case where the given dictionary has keys for properties that don't exist. The default
implementation by NSObject
simply throws an NSUndefinedKeyException
,
which is almost certainly not what you want (Pitfall #2).
Another pitfall with KVC is a lack of type checking (Pitfall #3). This behavior
follows naturally from Objective-C's dynamic nature, however it will definitely lead to very
bizarre bugs further in the development process. KVC will happily let you assign an
NSNumber
to an NSString
property at runtime, and you will be
scratching your head when you later try to determine the length of that string and the app
immediately crashes. For example, an NSString
property name
on an
Employee
bill
:
[bill setValue:[NSNumber numberWithInteger:1] forKey:@"name"]; // compiles and runs bill.name = [NSNumber numberWithInteger:1]; // won't compile: Incompatible pointer types assigning to 'NSString *' from 'NSNumber *'Because the
value
parameter in setValue:forKey:
is id
,
it will accept any pointer you send it, and the discrepancy will only come to light when you
use the value. Inadvertent type changes can commonly happen when an API switches JSON parsers
or changes something internally and different values are generated for the same keys.
KVC includes support for validation that could catch these type mismatches, but it requires
explicit coding on your part (Pitfall #4). NSKeyValueCoding
includes
validateValue:forKey:error:
and validateValue:forKeyPath:error:
to
help you with any custom validation logic you want to implement. The default implementations
will search for a method that has the pattern validate{key name}:error:
and call
it if it exists. While this behavior is handy, it can be very tedious to add validation to
a class with a dozen or more properties.
The other drawback with the built-in validation is that the validation logic is not
automatically run (Pitfall #5). The expectation is that as the developer, you will
call either validateValue:forKey:
before setting a key's value or
validate{key name}:error:
yourself. While there may be some specific cases that
you would want this flexibility, in general the whole process can be easily overlooked,
yielding invalid data.
@property (nonatomic, strong) NSString firstName; - (BOOL)validateFirstName:(id*)ioValue error:(NSError**)error { // Validation logic goes here } Person *p = [Person new]; NSString *firstName = @"Bill"; NSError *error = nil; // This call below actually calls our validateFirstName: error: method if ([p validateValue:&firstName forKey:@"firstName" error:&error]) { [p setValue:firstName forKey:@"firstName"]; }As you can see, that is a lot of code to validate just a single property before setting it. If
Person
had twenty properties, imagine the amount of code you would have to add
to this class to validate all of them. Now consider writing that code for every class in the
project, and you can see how quickly you would get bogged down just writing validation logic.
Now that you have an understanding of KVC's built-in validation, we can expand on it and improve the five pitfalls we identified. Our pattern's goals:
validate{key name}:
methodsTo tackle case insensitivity, any class inheriting from the base model object will
determine its own properties and their types, and repeat the process for its superclass(es).
For each property, it will build a mapping dictionary that maps the lowercase version of
the property name to the actual property name. Whenever we need to operate on an
ambiguously-cased key, we can simply lowercase it and look up the real name in this mapping
dictionary. The base model class has a static NSDictionary
that stores this
property information for each distinct subclass. The population of these class-specific
details happens in initialize
because of it will always be called before the
class is used:
initialize
Initializes the receiver before it’s used (before it receives its first message).
+ (void)initialize
Discussion
The runtime sendsinitialize
to each class in a program exactly one time just before the class, or any class that inherits from it, is sent its first message from within the program. (Thus the method may never be invoked if the class is not used.) The runtime sends theinitialize
message to classes in a thread-safe manner. Superclasses receive this message before their subclasses.
+ (void)initialize { [super initialize]; dispatch_once(&onceToken, ^{ modelProperties = [NSMutableDictionary dictionary]; propertyTypesArray = @[/* removed for brevity */]; }); NSMutableDictionary *translateNameDict = [NSMutableDictionary dictionary]; [self hydrateModelProperties:[self class] translateDictionary:translateNameDict]; [modelProperties setObject:translateNameDict forKey:[self calculateClassName]]; } + (void)hydrateModelProperties:(Class)class translateDictionary:(NSMutableDictionary *)translateDictionary { if (!class || class == [NSObject class]){ return; } unsigned int outCount, i; objc_property_t *properties = class_copyPropertyList(class, &outCount); for (i = 0; i < outCount; i++){ objc_property_t p = properties[i]; const char *name = property_getName(p); NSString *nsName = [[NSString alloc] initWithCString:name encoding:NSUTF8StringEncoding]; NSString *lowerCaseName = [nsName lowercaseString]; [translateDictionary setObject:nsName forKey:lowerCaseName]; NSString *propertyType = [self getPropertyType:p]; [self addValidatorForProperty:nsName type:propertyType]; } free(properties); [self hydrateModelProperties:class_getSuperclass(class) translateDictionary:translateDictionary]; }Note that
hydrateModelProperties:translateDictionary:
is called recursively until
it hits the base class (where class
is nil
) or NSObject
.
Now if a class needs to set keys like "firstName" or "firstname" or "FIRSTNAME", each will get
lowercased and matched to the real property. The ambiguously-cased value can be sent into
setValue:forKey:
, which will determine the proper case automatically:
- (void)setValue:(id)value forKey:(NSString *)key { [self setValue:value forKey:key properCase:NO]; } - (void)setValue:(id)value forKey:(NSString *)key properCase:(BOOL)properCase { if (!properCase) { NSString *lowerCaseKey = [key lowercaseString]; NSString *properKey = modelProperties[self.dictionaryKey][lowerCaseKey]; if (properKey){ key = properKey; } } [super setValue:value forKey:key]; }
Checking each value before setting it was goal #2, and it also uses some runtime logic to
determine the type of each property and hooks up the appropriate validator for that type.
The type determination is done in initialize
at the same time as the property
names are queried. The Objective-C runtime returns property types from
property_getName(objc_property_t property)
, and the values differ between primitive
types and objects. Objects return a C-string of the class name, while primitive types return
the value given by the @encode()
function. Our pattern uses the
propertyTypesArray
to convert between the return values and an enum
CTCPropertyType
. The array is declared such that the value of the enum matches
the index of the equivalent C-string:
typedef NS_ENUM(NSUInteger, CTCPropertyType){ CTCPropertyUnknown = 0, // Property type is unknown CTCPropertyTypeString, // Property is an NSString CTCPropertyTypeBool, // Property is a BOOL CTCPropertyTypeNumber, // Property is an NSNumber CTCPropertyTypeInteger, // Property is an NSInteger CTCPropertyTypeFloat, // Property is a float CTCPropertyTypeArray, // Property is an NSArray CTCPropertyTypeMutableArray, // Property is an NSMutableArray CTCPropertyTypeDictionary, // Property is an NSDictionary CTCPropertyTypeMutableDictionary, // Property is an NSMutableDictionary CTCPropertyTypeUnsignedInteger, // Property is an NSUInteger CTCPropertyTypeDate, // Property is an NSDate CTCPropertyTypeDouble // Property is a double }; // note this array's indexes MUST match the CTCPropertyType enum values for lookups to work properly propertyTypesArray = @[@"Unknown", @"NSString", [NSString stringWithFormat:@"%s",@encode(BOOL)], @"NSNumber", [NSString stringWithFormat:@"%s",@encode(int)], [NSString stringWithFormat:@"%s",@encode(float)], @"NSArray", @"NSMutableArray", @"NSDictionary", @"NSMutableDictionary", [NSString stringWithFormat:@"%s",@encode(unsigned int)], @"NSDate", [NSString stringWithFormat:@"%s",@encode(double)]];Once we have identified the matching enum value for each property, we hook up the appropriate
validate{key name}:error:
method by dynamically creating it on the spot. This
way we get the same behavior as the normal KVC validation without having to explicitly code
dozens of validate methods. It will also play nicely with any
validate{key name}:error:
methods that happen to already exist. For example,
the sample app's CTCStation
has some validate{key name}:error:
methods defined at compile time and some added at run time. The dynamic implementations
have been defined in ValidationFunctions.h
and follow the form:
BOOL validateStringProperty(id self, SEL _cmd, id *ioValue, NSError *__autoreleasing* outError){ CTCStringTypeValidator *validator = [CTCStringTypeValidator new]; return [validator validateValue:ioValue error:outError]; }The validators inherit from a base validator that has two actions: a validation action and a post validation action. Each is represented by a block, which allows you to tweak how it works without making a new subclass. The validation action returns the validated/scrubbed value or a default one and updates the
isValid
flag as appropriate:
self.defaultValidation = ^NSString *(id value, BOOL *isValid, NSError **error){ if ([value respondsToSelector:@selector(stringValue)]){ *isValid = YES; return [value stringValue]; } else { *isValid = NO; return nil; } };The
isValid
flag is important because it allows you to attempt to massage an
invalid value into a valid one. The example above shows turning any non-NSString
class that implements stringValue
(say NSNumber
) into the equivalent
string. Because we were able to do this manipulation, the app is able to receive invalid
data but still perform as if we had received what we expected. If we aren't able to fix
the invalid data, we don't have any choice but to set isValid
to NO
and give up. Note that the defaultValidation
block does not need to do a type
check because the super implementation does one before calling the defaultValidation
block:
- (BOOL)validateValue:(id *)value error:(NSError **)error { _isValid = [*value isKindOfClass:[NSString class]]; return [super validateValue:value error:error]; }A good example of the post-validation action is in this zip code validator that ensures any zip code less than 5 digits is padded with zeroes:
_zipValidator = [CTCStringTypeValidator new]; _zipValidator.postValidation = ^NSString *(id value){ value = [NSString leftPadString:value length:5 padCharacter:@"0"]; return value; };Notice that it is actually a string validator and didn't require a special subclass because we simply changed the block's logic. Also notice that there is no type checking or validation of any kind in the
postValidation
block. This is because the post-validation
logic is only run if the value is valid. The block is not intended to make any changes
to the value itself, but perhaps to format it for display to the user. Other examples might
be formatting a price string with an NSNumberFormatter
or ensuring that a state
abbreviation is always shown in uppercase.setValue:forKey:properCase:
, which finally sets the scrubbed and validated value
using the super class's (which is NSObject
) setValue:forKey:
if the
value has been deemed valid. If the validator was unable to validate the value, the value is
not set at all.
We know it can be confusing to follow the line of execution between the model object, validators, and KVC itself, so we put together an example app that uses the entire pattern. If you understand the individual pieces but aren't clear how they fit together, the "Sample Validation App" section is just for you.
In certain scenarios an object might logically have a data element with the same name as a
reserved Objective-C keyword (commonly "id" or "description") or a name that doesn't fit with
how you have your object structured. KVC provides setValue:forUndefinedKey:
for
those scenarios, but the mapping process has to be done manually in each subclass. In our
pattern, the base model object has an undefinedKeys
dictionary that maps from
the received key name to the actual property name. Some of these mappings can be done globally
and others can be set by specific subclasses. A good candidate for a global mapping would be
changing "id" to "identifier" and a specific case might be to map "description" to "name":
- (NSDictionary *)undefinedKeys { static NSDictionary *undefinedKeys; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ undefinedKeys = @{@"description": @"stationName"}; }); return undefinedKeys; }It's important to note that the key should always be lowercase and the mapped property name should be cased the same way as the actual property definition. The implementation for
setValue:forUndefinedKey:
simply looks up the given key in the
undefinedKeys
dictionary and ignores any it does not find.
- (void)setValue:(id)value forUndefinedKey:(NSString *)key { NSString *newKey = self.undefinedKeys[[key lowercaseString]]; if (newKey){ [self setValue:value forKey:newKey properCase:YES]; } }
In cases where the data can be manipulated into the expected format, we do that conversion, but in the worst case we assign a default value that won't crash the app or leave holes in the user interface. When running the app you should see identical UI between Response A, B, and D; Response C should be the same but have a price of $4.00; and Response E should have only default/blank data. These represent scenarios of increasing divergence between the data structure the app expects versus what is present in the JSON itself.
The next sections will walk through the execution path for validation of certain properties in the stubbed JSON responses. Calls in blue are ones that would be made using standard KVC and all others are specific to this pattern.
The following JSON is the happy path description of a gas station. Each property is the
proper type and value the app expects, and this response will be the baseline against which to
compare responses B through E.
{ "description":"7/11 Fake St.", "price":3.50, "sellsDiesel": true, "address": { "street":"123 Fake Street", "city":"Newark", "state":"NJ", "zip":"07105" }, "historicalPrices":[ { "date":"2013-02-18T15:43:24-05:00", "price":4.60 } ] }We are going to trace the execution path of the "price" element and then compare that to extra mapping that is required for "description". This pseudo stack trace is most helpful if you have the code open as well and can jump through the source files as you go through each step. Remember that calls in blue are ones that would be made using standard KVC and all others are specific to this pattern.
CTCStation
– initWithDictionary:<NSDictionary
including "price"=3.5>
NSObject<NSKeyValueCoding>
–
setValuesForKeysWithDictionary:<NSDictionary including "price"=3.5>
CTCBaseModel
–
setValue:<NSNumber: 3.5> forKey:@"price"
CTCBaseModel
– setValue:<NSNumber: 3.5>
forKey:@"price" properCase:NO
CTCBaseModel
– validateValue:<NSNumber: 3.5>
forKey:@"price" error:<NSError>
CTCStation
–
validatePrice:<NSNumber: 3.5> error:<NSError>
CTCNumberTypeValidator
– validateValue:<NSNumber:
3.5> error:<NSError>
isValid
was set to YES
here because the types matchCTCBaseValidator
– validateValue:<NSNumber: 3.5>
error:<NSError>
isValid
is YES
and calls the
postValidation
block if necessaryNSObject<NSKeyValueCoding>
–
setValue:<NSNumber: 3.5> forKey:@"price"
CTCStation
–
setPrice:3.5f
price
property becomes
3.5f
CTCStation
initializes the undefinedKeys to map it to the stationName
property:
undefinedKeys = @{@"description": @"stationName"};
CTCStation
– initWithDictionary:<NSDictionary including
"description"="7/11 Fake St.">
NSObject<NSKeyValueCoding>
–
setValuesForKeysWithDictionary:<NSDictionary including
"description"="7/11 Fake St.">
CTCBaseModel
–
setValue:@"7/11 Fake St." forUndefinedKey:@"description"
undefinedKeys
dictionary is used here to find the new key name
"stationName". Because the programmer intentionally set up this mapping, we assume
it is already properly-cased.CTCBaseModel
– setValue:@"7/11 Fake St."
forKey:@"stationName" properCase:YES
CTCBaseModel
– validateValue:@"7/11 Fake St."
forKey:@"stationName" error:<NSError>
CTCStation
–
validateStationName:@"7/11 Fake St." error:<NSError>
CTCStringTypeValidator
– validateValue:@"7/11 Fake St."
error:<NSError>
isValid
was set to YES
here because the types matchCTCBaseValidator
– validateValue:@"7/11 Fake St."
error:<NSError>
isValid
is YES
and calls the
postValidation
block if necessaryNSObject<NSKeyValueCoding>
–
setValue:@"7/11 Fake St." forKey:@"stationName"
CTCStation
–
setStationName:@"7/11 Fake St."
stationName
property becomes
@"7/11 Fake St."Response B is logically equivalent to Response A, however four elements have changed their
types.
{ "description":"7/11 Fake St.", "price":"$3.50", "sellsDiesel": "yes", "address": { "street":"123 Fake Street", "city":"Newark", "state":"NJ", "zip":7105 }, "historicalPrices":[ { "date":"2013-02-18T15:43:24-05:00", "price":"$4.60" } ] }Notable changes:
CTCStation
– initWithDictionary:<NSDictionary including
"price"="$3.50">
NSObject<NSKeyValueCoding>
–
setValuesForKeysWithDictionary:<NSDictionary including
"price"="$3.50">
CTCBaseModel
- setValue:@"$3.50"
forKey:@"price"
CTCBaseModel
– setValue:@"$3.50" forKey:@"price"
properCase:NO
CTCBaseModel
– validateValue:@"$3.50" forKey:@"price"
error:<NSError>
CTCStation
–
validatePrice:@"$3.50" error:<NSError>
CTCNumberTypeValidator
– validateValue:@"$3.50"
error:<NSError>
isValid
was set to NO
here because the types
(NSNumber
vs. NSString
) did not matchCTCBaseModelValidator
– validateValue:@"$3.50"
error:<NSError>
CTCFloatTypeValidator
–
defaultValidation(@"$3.50", NO, <NSError>)
blockfloatValue
, calls it, and wraps the result in an
NSNumber
.NSObject
–
setValue:<NSNumber: 3.5> forKey:@"price"
CTCStation
–
setPrice:3.5f
price
property becomes
3.5f
isValid
is NO
in this case, and
value is sent to the validator to be manipulated (via the floatValue
call) into
the type we expect.
The execution logic for "sellsDiesel" is also interesting because it demonstrates how the
boolean validator is able to transform certain strings into valid YES
or
NO
values:
CTCStation
– initWithDictionary:<NSDictionary
including "sellsDiesel"="yes">
NSObject<NSKeyValueCoding>
–
setValuesForKeysWithDictionary:<NSDictionary including
"sellsDiesel"="yes">
CTCBaseModel
– setValue:@"yes"
forKey:@"sellsDiesel"
CTCBaseModel
– setValue:@"yes" forKey:@"sellsDiesel"
properCase:NO
CTCBaseModel
– validateValue:@"yes" forKey:@"sellsDiesel"
error:<<NSError>
CTCStation
–
validateSellsDiesel:@"yes" error:<NSError>
CTCNumberTypeValidator
– validateValue:@"yes"
error:<NSError>
isValid
was set to NO
here because the types
(NSNumber
vs. NSString
) did not matchCTCBaseModelValidator
– validateValue:@"yes"
error:<NSError>
CTCBooleanTypeValidator
–
defaultValidation(@"yes", NO, <NSError>)
blockNSString
matches one of our predefined
YES
equivalent strings, then wraps the YES
in an
NSNumber
.NSObject
–
setValue:<NSNumber: YES> forKey:@"sellsDiesel"
CTCStation
–
setSellsDiesel:YES
sellsDiesel
property becomes
YES
Response C takes type changes even further to demonstrate additional flexibility in the
various validator classes.
{ "description":"7/11 Fake St.", "price":4, "sellsDiesel": 1, "address": { "street":"123 Fake Street", "city":"Newark", "state":"NJ", "zip":7105 }, "historicalPrices":[ { "date":1361202204, "price":4.6 } ] }Notable changes:
Response D shows how a validator responds to input with unexpected structure.
{ "description":"7/11 Fake St.", "price":3.5, "sellsDiesel": true, "address": { "street":"123 Fake Street", "city":"Newark", "state":"NJ", "zip":"07105" }, "historicalPrices":{ "date":"2013-02-18T15:43:24-05:00", "price":4.60 } }This scenario happens more than you might expect with some JSON generators. Some systems (e.g. Jettison) that convert from other formats will see a single element in a collection and output that as a solitary element instead of an array that contains one element.
CTCStation
uses a normal KVC validation method,
validateHistoricalPrices:error:
, to handle this case, but does the validation
logic using an array validator with custom logic. It assumes that if the type is not an array,
then it should be parsed as if it was an array containing the solitary element.
- (BOOL)validateHistoricalPrices:(id *)ioValue error:(NSError *__autoreleasing *)outError{ dispatch_once(&_historyToken, ^{ // if this happens to be a single object (i.e. a dictionary) then assume it was supposed to be an array of one element _historyValidator = [[CTCArrayTypeValidator alloc] initWithDefaultValidation:^NSArray *(id value, BOOL *isValid, NSError **error){ if ([value isKindOfClass:[NSDictionary class]]){ *isValid = YES; return @[value]; } return nil; }]; // take the array we are given and process its contents as CTCHistoricalPrice objects _historyValidator.postValidation = ^NSArray *(id value){ NSMutableArray *histories = [NSMutableArray array]; for (NSDictionary *dict in value){ CTCHistoricalPrice *price = [[CTCHistoricalPrice alloc] initWithDictionary:dict]; [histories addObject:price]; } return histories; }; }); return [_historyValidator validateValue:ioValue error:outError]; }
Response E represents the case where the actual JSON structure has diverged so much from
the expected structure that the app can no longer overcome the differences with flexible
validation logic.
null
values and error messages simulate the catastrophic failure of some
backend system:
{ "description":null, "price":null, "sellsDiesel": null, "address":"error looking up address", "historicalPrices":"error fetching prices" }In this case the app has no chance of giving the user the data he or she requested, but the least it can do is populate default values and ensure it doesn't crash. The various validators each define a default value that is used in place of invalid values that can't be manipulated into a valid value. Typically these values are the logical equivalent of 0 or
nil
for
that property type, however different values can always be substituted if certain situations
require them.
In this post, we covered the basics of KVC and KVC validation. We talked about some of the inherent issues with KVC validation and how it works. We identified and implemented four goals for an improved validation pattern: case-insensitivity for key name, type checking, automatic creation of property validation methods, and easy remapping of keys to properties with different names. A sample app demonstrates the improved pattern in the context of crowd-sourced gas price tracking, where flexible validation logic can recover from the unpredictability of the available data.
The sample app can be found on GitHub at https://github.com/jszumski/kvc-validation-pattern.