Validating Data with TWTValidation

 ·  iOS, Development, Open-Source  ·  Tagged open-source, TWTValidation, Validation, Key-Value Coding and Key-Value Validation

A few months ago, Andrew and I were working on a project that required some validation of model objects and form data. After looking around a bit, we quickly came to the conclusion that there aren’t any widely accepted best practices for data validation in Cocoa. If you’re using Core Data, you can declare some validators for strings and numbers in your data model. If you’re not, you have to roll your own validation system, perhaps using Key-Value Validation (KVV), a part of Foundation that many experienced Cocoa developers have never even heard of.

There’s a huge gap between these two options. Core Data’s managed object validation is declarative: you don’t describe how to perform validation, you just declare what validations need to take place. Key-Value Validation is the exact opposite: you override ‑validateValue:​forKey:​error: (or ‑validate«Key»:​error:) and write the code that actually performs the validations. What we set out to do was to bring the simplicity and clarity of Core Data’s declarative approach to non-Core Data classes. We think we’ve done that with TWTValidation.

TWTValidation

TWTValidation is a Cocoa framework that allows you to easily validate data with reusable validator objects, each of which is a subclass of TWTValidator. The common interface for all validators is a single method: ‑validateValue:​error:. This method returns whether a given value is valid and, if not, indirectly returns an error describing why validation failed. With just this simple interface, we’ve been able to build some pretty interesting validators. While going over each of them would take too much time, here’s a brief summary:

  • Value validators can validate that a value is of a particular class. They can also optionally allow for values that are nil or the NSNull instance.
    • Number validators can check if a number is within a given range. You can also optionally require that a value be an integer.
    • String validators are value validators that validate strings. There are a variety of string validators for doing things like checking the length of a string or validating that it matches a particular pattern.
  • Compound validators allow you to combine multiple validators using logical operators like AND, OR, and NOT.
  • Collection validators validate the count and elements of arrays and sets.
  • Keyed collection validators validate the count, keys, values, and specific key-value pairs for dictionaries and map tables.
  • Block validators allow you to easily create a TWTValidator with custom validation logic using a block.

These validators are incredibly useful—see the [TWTValidation readme][Readme] for code examples—and fundamentally important to any use of TWTValidation, but my favorite class in the framework is TWTKeyValueCodingValidator.

Key-Value Coding Validators

Key-value coding (KVC) validators validate an object using validators that the object itself provides. Each KVC validator is initialized with a set of KVC keys.

NSSet *keys = [NSSet setWithObjects:@"property1", @"property2", nil];
TWTKeyValueCodingValidator *validator = [[TWTKeyValueCodingValidator alloc] initWithKeys:keys];

Later, when the validator validates an object, it iterates over each key in its key set, gets the object’s value for that key (using ‑valueForKey:), gets the object’s validators for that key, and then validates the key’s value. In pseudo-code, this looks like:

for (NSString *key in self.keys) {
    id value = [object valueForKey:key];

    // Get validators from object somehow
    NSSet *validators =  ;

    // Run the validators on value, accumulating and returning any errors
}

How does it get object’s validators for a key? TWTKeyValueCodingValidator declares an informal protocol containing the method +twt_validatorsForKey:, which classes can override to return the set of TWTValidator objects for a given KVC key. For example,

+ (NSSet *)twt_validatorsForKey:(NSString *)key
{
    if ([key isEqualToString:@"property1"]) {
        // Create and return an NSSet containing the validators for property1
    } else if ([key isEqualToString:@"property2"]) {
        // Create and return an NSSet containing the validators for property2
    }

    // Otherwise, ask the superclass
    return [super twt_validatorsForKey:key];
}

Unfortunately, cascading if-else statements like this are sort of gross, particularly when you have a lot of keys you want to validate. We remedy this by borrowing a simple pattern from Key-Value Coding, Key-Value Observing, and Key-Value Validation. The base implementation of +twt_validatorsForKey: checks to see if the receiver responds to +twt_validatorsFor«Key», where «Key» is the capitalized form of the key parameter. If so, we simply return the result of that method; otherwise, we returns nil. Knowing this, we can now simplify the cascading if-else into:

+ (NSSet *)twt_validatorsForProperty1
{
    // Create and return an NSSet containing the validators for property1
}

+ (NSSet *)twt_validatorsForProperty2
{
    // Create and return an NSSet containing the validators for property2
}

To my eyes, this is a lot clearer. Basically, if you implement these methods, validation of the property1 and property2 keys will just work. Any other keys will not be validated, i.e., they will implicitly pass, because you haven’t declared any validators for them.

Let’s walk through a complete example just to drive this whole idea home.

Example: Validating a User Object

For our example, we’ll create a simple class that models users for some imaginary online service.

@interface TWTUser : NSObject

@property (nonatomic, strong) NSNumber *ID;
@property (nonatomic, strong) NSString *fullName;
@property (nonatomic, copy) NSString *username;

@end

User IDs must be positive integers. Full names must be strings between 0 and 64 characters long. Usernames must only contain alphanumeric characters and '_' and must be between 1–15 characters long. To work with TWTKeyValueCodingValidators, we need to implement +twt_validatorsForID, +twt_validatorsForUsername, and +twt_validatorsForFullName. Let’s get to it.

Validating the ID is really simple. We just need to create a number validator that allows a minimum value of 1, no maximum, and requires integral values:

@implementation TWTUser

+ (NSSet *)twt_validatorsForID
{
    TWTNumberValidator *validator = [[TWTNumberValidator alloc] initWithMinimum:@1 maximum:nil];
    validator.requiresIntegralValue = YES;
    return [NSSet setWithObject:validator];
}

@end

Easy enough. Validating full names is very similar. We just need a string validator with a minimum length of 0 and a maximum length of 64. We’ll go ahead and allow nil for good measure.

+ (NSSet *)twt_validatorsForFullName
{
    TWTStringValidator *validator = [TWTStringValidator stringValidatorWithMinimumLength:0 maximumLength:64];
    validator.allowsNil = YES;
    return [NSSet setWithObject:validator];
}

Validating usernames is only slightly trickier. TWTValidation has support for validating strings using a regular expression. We can express the validation rule for usernames with the (unescaped) regular expression ^\w{1,15}$. This pattern only matches strings that are composed of 1–15 alphanumeric or underscore characters.

+ (NSSet *)twt_validatorsForUsername
{
    NSRegularExpression *usernameExpression = [[NSRegularExpression alloc] initWithPattern:@"^\w{1,15}$"
                                                                                   options:0
                                                                                     error:NULL];

    TWTStringValidator *validator = [TWTStringValidator stringValidatorWithRegularExpression:usernameExpression
                                                                                     options:0];
    return [NSSet setWithObject:validator];
}

Okay, so we have our validators declared. Now we just need to actually perform validation. Let’s add an ‑isValid method to TWTUser for that purpose. Internally, it will use a KVC validator to actually perform the validation. We’ll store that in an internal property.

@interface TWTUser ()
@property (nonatomic, strong) TWTKeyValueCodingValidator *objectValidator;
@end


@implementation TWTUser

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSSet *keys = [NSSet setWithObjects:@"ID", @"fullName", @"username", nil];
        _objectValidator = [[TWTKeyValueCodingValidator alloc] initWithKeys:keys];
    }

    return self;
}

// ...

@end

Finally, we can implement ‑isValid:

- (BOOL)isValid
{
    return [self.objectValidator validateValue:self error:NULL];
}

And that’s it.

There are some important things to point out about our implementation. First, while we have our own private KVC validator, other KVC validators can be used as well. This might be desirable if you only wanted to validate the user’s username and ID, but not its full name.

We also aren’t limited to performing validation internally. If you have a controller that needs to perform some validation on a different key set, it can do that using its own KVC validator, which will still get the appropriate validators from the TWTUser object.

Finally, KVC validators can co-exist with KVV. For example, suppose that we didn’t implement +twt_validatorsForFullName and instead had an equivalent KVV method:

- (BOOL)validateFullName:(NSString **)ioValue error:(NSError **)outError
{
    if (!*ioValue || ([*ioValue isKindOfClass:[NSString class]] && [*ioValue length] <= 16)) {
        return YES;
    } else if (outError) {
        // Construct an NSError and assign it to outError
    }

    return NO;
}

When the KVC validators finds that you have no validators declared for fullName, it falls back on key-value validation to do its work. In other words, everything just works. This can be useful if you want to transition your code from KVV to TWTValidation.


So, that’s a taste of TWTValidation. We think it’s a great approach to doing data validation in Cocoa. We’ve only covered a small part of the framework, so read the docs and take a look at our GitHub project page. We’re also hard at work on the next release of TWTValidation, so be on the lookout for some updates in the next couple of weeks. If you have any questions or ideas for new validators, I’m @prachigauriar on Twitter!