Managed Object Validation

There are two types of validation—property-level and inter-property. You use property-level validation to ensure the correctness of individual values; you use inter-property validation to ensure the correctness of combinations of values.

Core Data Validation

Cocoa provides a basic infrastructure for model value validation. It requires you, though, to write code for all the constraints you want to apply. Core Data allows you to put validation logic into the managed object model. You can specify maximum and minimum values for numeric and date attributes; maximum and minimum lengths for string attributes, and a regular expression that a string attribute must match. You can also specify constraints on relationships, for example that they are mandatory or cannot exceed a certain number. You can therefore specify most common constraints on attribute values without writing any code.

If you do want to customize validation of individual properties, you use standard validation methods as defined by the NSKeyValueCoding protocol and described in “Property-Level Validation”). Core Data also extends validation to validation of relationships and inter-property values. These are described in “Inter-Property validation.”

It is important to understand that how to validate is a model decision, when to validate is a user interface or controller-level decision (for example, a value binding for a text field might have its “validates immediately” option enabled). Moreover, at various times, inconsistencies are expected to arise in managed objects and object graphs.

There is nothing to disallow an in-memory object from becoming inconsistent on a temporary basis. The validation constraints are applied by Core Data only during a “save” operation or upon request (you can invoke the validation methods directly as and when you wish). Sometimes it may be useful to validate changes as soon as they are made and to report errors immediately. This can prevent the user being presented with a long list of errors when they finally come to save their work. If managed objects were required to be always in a valid state, it would amongst other things force a particular workflow on the end-user. This also underpins the idea of a managed object context representing a "scratch pad"—in general you can bring managed objects onto the scratch pad and edit them however you wish before ultimately either committing the changes or discarding them.

Property-Level Validation

The NSKeyValueCoding protocol specifies a method—validateValue:forKey:error:—that provides general support for validation methods in a similar way to that in which valueForKey: provides support for accessor methods.

If you want to implement logic in addition to the constraints you provide in the managed object model, you should not override validateValue:forKey:error:. Instead you should implement methods of the form validate<Key>:error:.

In the method implementation, you check the proposed new value and if it does not fit your constraints you return NO. If the error parameter is not null, you also create an NSError object that describes the problem, as illustrated in this example.

-(BOOL)validateAge:(id *)ioValue error:(NSError **)outError {
 
    if (*ioValue == nil) {
        // trap this in setNilValueForKey? new NSNumber with value 0?
        return YES;
    }
    if ([*ioValue floatValue] <= 0.0) {
        if (outError != NULL) {
            NSString *errorStr = NSLocalizedStringFromTable(
                @"Age must greater than zero", @"Employee",
                @"validation: zero age error");
            NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorStr };
            NSError *error = [[NSError alloc] initWithDomain:EMPLOYEE_ERROR_DOMAIN
                code:PERSON_INVALID_AGE_CODE
                userInfo:userInfoDict];
            *outError = error;
        }
        return NO;
    }
    else {
        return YES;
    }
    // . . .

The input value is a pointer to object reference (an id *). This means that in principle you can change the input value. Doing so is, however, strongly discouraged, as there are potentially serious issues with memory management (see “Key-Value Validation” in Key-Value Coding Programming Guide). Moreover, you should not call validateValue:forKey:error: within custom property validation methods. If you do, you will create an infinite loop when validateValue:forKey:error: is invoked at runtime.

If you change the input value in a validate<Key>:error: method, you must ensure that you only change the value if it is invalid or uncoerced. The reason is that, since the object and context are now dirtied, Core Data may validate that key again later. If you keep performing a coercion in a validation method, this can therefore produce an infinite loop. Similarly, you should also be careful if you implement validation and willSave methods that produce mutations or side effects—Core Data will revalidate those changes until a stable state is reached.

Inter-Property validation

It is possible for the values of all the individual attributes of an object to be valid and yet for the combination of values to be invalid. Consider, for example, an application that stores information about people including their age and whether or not they have a driving license. For a Person object, 12 might be a valid value for an age attribute, and YES is a valid value for a hasDrivingLicense attribute, but (in most countries at least) this combination of values would be invalid.

NSManagedObject provides additional opportunities for validation—update, insertion, and deletion—through the validateFor… methods such as validateForUpdate:. If you implement custom inter-property validation methods, you call the superclass’s implementation first to ensure that individual property validation methods are also invoked. If the superclass's implementation fails (that is, if there is an invalid attribute value), then you can:

  1. Return NO and the error created by the superclass's implementation.

  2. Continue to perform validation, looking for inconsistent combinations of values.

If you continue, you must make sure that any values you use in your logic are not themselves invalid in such a way that your code might itself cause errors (for example, if there is an attribute whose value is required to be greater than 0, which is actually 0 so fails validation but which you use as a divisor in a computation). Moreover, if you discover further validation errors, you must combine them with the existing error and return a “multiple errors error” as described in “Combining Validation Errors.”

The following example shows the implementation of an inter-property validation method for a Person entity that has two attributes, birthday and hasDrivingLicense. The constraint is that a person aged less than 16 years cannot have a driving license. This constraint is checked in both validateForInsert: and validateForUpdate:, so the validation logic itself is factored into a separate method.

Listing 1  Inter-property validation for a Person entity

- (BOOL)validateForInsert:(NSError **)error
{
    BOOL propertiesValid = [super validateForInsert:error];
    // could stop here if invalid
    BOOL consistencyValid = [self validateConsistency:error];
    return (propertiesValid && consistencyValid);
}
 
- (BOOL)validateForUpdate:(NSError **)error
{
    BOOL propertiesValid = [super validateForUpdate:error];
    // could stop here if invalid
    BOOL consistencyValid = [self validateConsistency:error];
    return (propertiesValid && consistencyValid);
}
 
 
- (BOOL)validateConsistency:(NSError **)error
{
    static     NSCalendar *gregorianCalendar;
 
    BOOL valid = YES;
    NSDate *myBirthday = [self birthday];
 
    if ((myBirthday != nil) && ([[self hasDrivingLicense] boolValue] == YES)) {
 
        if (gregorianCalendar == nil) {
            gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
        }
        NSDateComponents *components = [gregorianCalendar components:NSYearCalendarUnit
                                                            fromDate:myBirthday
                                                              toDate:[NSDate date]
                                                             options:0];
        int years = [components year];
 
        if (years < 16) {
 
            valid = NO;
 
            // don't create an error if none was requested
            if (error != NULL) {
 
                NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
                NSString *drivingAgeErrorString = [myBundle localizedStringForKey:@"TooYoungToDriveError"
                                  value:@"Person is too young to have a driving license."
                                  table:@"PersonErrorStrings"];
 
                NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
                [userInfo setObject:drivingAgeErrorString forKey:NSLocalizedFailureReasonErrorKey];
                [userInfo setObject:self forKey:NSValidationObjectErrorKey];
 
                NSError *drivingAgeError = [NSError errorWithDomain:PERSON_DOMAIN
                                                               code:NSManagedObjectValidationError
                                                           userInfo:userInfo];
 
                // if there was no previous error, return the new error
                if (*error == nil) {
                    *error = drivingAgeError;
                }
                // if there was a previous error, combine it with the existing one
                else {
                    *error = [self errorFromOriginalError:*error error:drivingAgeError];
                }
            }
        }
    }
    return valid;
}

Combining Validation Errors

If there are multiple validation failures in a single operation, you create and return a "multiple errors error"—that is, an NSError object with the code NSValidationMultipleErrorsError. You add individual errors to an array and add the array—using the key NSDetailedErrorsKey—to the user info dictionary in the NSError object. This pattern also applies to errors returned by the superclass's validation method. Depending on how many tests you perform, it may be convenient to define a method that combines an existing NSError object (which may itself be a multiple errors error) with a new one and returns a new multiple errors error.

The following example shows the implementation of a simple method to combine two errors into a single multiple errors error. How the combination is made depends on whether or not the original error was itself a multiple errors error.

Listing 2  A method for combining two errors into a single multiple errors error

- (NSError *)errorFromOriginalError:(NSError *)originalError error:(NSError *)secondError
{
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    NSMutableArray *errors = [NSMutableArray arrayWithObject:secondError];
 
    if ([originalError code] == NSValidationMultipleErrorsError) {
 
        [userInfo addEntriesFromDictionary:[originalError userInfo]];
        [errors addObjectsFromArray:[userInfo objectForKey:NSDetailedErrorsKey]];
    }
    else {
        [errors addObject:originalError];
    }
 
    [userInfo setObject:errors forKey:NSDetailedErrorsKey];
 
    return [NSError errorWithDomain:NSCocoaErrorDomain
                               code:NSValidationMultipleErrorsError
                           userInfo:userInfo];
}