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
Property-Level Validation
Inter-Property validation
Combining Validation Errors
Cocoa provides a basic infrastructure for model value validation described in Model Object Validation in Model Object Implementation Guide. This approach, however, requires you 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.
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:
.
Important: If you do implement custom validation methods, you should typically not invoke them directly. Instead you should call validateValue:forKey:error:
with the appropriate key. This ensures that any constraints defined in the managed object model are also applied.
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 = [NSDictionary dictionaryWithObject:errorStr |
forKey:NSLocalizedDescriptionKey]; |
NSError *error = [[[NSError alloc] initWithDomain:EMPLOYEE_ERROR_DOMAIN |
code:PERSON_INVALID_AGE_CODE |
userInfo:userInfoDict] autorelease]; |
*outError = error; |
} |
return NO; |
} |
else { |
return YES; |
} |
// . . . |
It is important to note that 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). 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.
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 loci 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:
Return NO
and the error created by the superclass's implementation.
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; |
} |
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]; |
} |
© 2004, 2009 Apple Inc. All Rights Reserved. (Last updated: 2009-03-04)