Documentation Archive

Developer

Core Data Programming Guide

On This Page

Object Validation

Cocoa provides a basic infrastructure for model value validation. However, it requires you to write code for all the constraints you want to apply. Core Data, on the other hand, allows you to put validation logic into the managed object model and specify most common constraints as opposed to writing validation logic in your code. 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, such as making them mandatory or unable exceed a certain number.

If you do want to customize validation of individual properties, you use standard validation methods as defined by the NSKeyValueCoding protocol and described in Implementing Custom Property-Level Validation. To validate combinations of values (such as an array) and relationships, see Implementing Custom Interproperty Validation.

How Validation Works in Core Data

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.

An in-memory object can temporarily become inconsistent. The validation constraints are applied by Core Data only during a save operation or upon request (you can invoke the validation methods directly at any time it makes sense for your application flow). Sometimes it is useful to validate changes as soon as they are made and to report errors immediately. Other times it makes sense to wait until a unit of work is completed before validation takes place. If managed objects were required to be always in a valid state, it would among other things force a particular workflow on the user. The ability to work with managed objects when they are not in a valid state 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 committing the changes or discarding them.

Implementing Custom Property-Level Validation

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

NSManagedObject provides consistent hooks for implementing property (and interproperty) values. If you want to implement logic in addition to the constraints you provide in the managed object model, do not override validateValue:forKey:error:. Instead implement methods of the form validate<Key>:error:. If you do implement custom validation methods, you should typically not invoke them directly. Instead call the general method validateValue:forKey:error: with the appropriate key. This ensures that any constraints defined in the managed object model are also applied. If you were to call validate<Key>: error: instead, constraints may not be applied.

In the method implementation, you check the proposed new value, and if it does not fit your constraints, you return NOfalse. If the error parameter is not null, you also create an NSError object that describes the problem, as illustrated in the following example. The example validates that the age value is greater than zero. If it is not, an error is returned.

Objective-C

  1. - (BOOL)validateAge:(id*)ioValue error:(NSError**)outError
  2. {
  3. if (*ioValue == nil) {
  4. return YES;
  5. }
  6. if ([*ioValue floatValue] <= 0.0) {
  7. if (outError == NULL) {
  8. return NO;
  9. }
  10. NSString *errorStr = NSLocalizedStringFromTable(@"Age must be greater than zero", @"Employee", @"validation: zero age error");
  11. NSDictionary *userInfoDict = @{NSLocalizedDescriptionKey: errorStr};
  12. NSError *error = [[NSError alloc] initWithDomain:EMPLOYEE_ERROR_DOMAIN code:PERSON_INVALID_AGE_CODE userInfo:userInfoDict];
  13. *outError = error;
  14. return NO;
  15. } else {
  16. return YES;
  17. }
  18. }

Swift

  1. func validateAge(value: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
  2. if value == nil {
  3. return
  4. }
  5. let valueNumber = value?.pointee as! NSNumber
  6. if valueNumber.floatValue > 0.0 {
  7. return
  8. }
  9. let errorStr = NSLocalizedString("Age must be greater than zero", tableName: "Employee", comment: "validation: zero age error")
  10. let userInfoDict = [NSLocalizedDescriptionKey: errorStr]
  11. let error = NSError(domain: "EMPLOYEE_ERROR_DOMAIN", code: 1123, userInfo: userInfoDict)
  12. throw error
  13. }

The input value is a pointer to an object reference (an id *). This means that in principle you can change the input value. However, doing so is strongly discouraged because there are potentially serious issues with memory management (see Key-Value Validation in Key-Value Coding Programming Guide). Moreover, do 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.

Do not change the input value in a validate<Key>:error: method unless the value is invalid or uncoerced. The reason is that, because 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, it can produce an infinite loop. Similarly, 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.

Implementing Custom Interproperty 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 people’s 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 YEStrue 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 interproperty 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), you can do one of the following:

  • Return NOfalse and the error created by the superclass's implementation.

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

If you continue to perform validation, 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, suppose you use an attribute whose value is 0 as a divisor in a computation, but the attribute is required to have a value greater than 0. 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 interproperty validation method for a Person entity that has two attributes, birthday and hasDrivingLicense. The constraint is that a person younger 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 14-1Interproperty validation for a Person entity

Objective-C

  1. - (BOOL)validateForInsert:(NSError **)error
  2. {
  3. BOOL propertiesValid = [super validateForInsert:error];
  4. // could stop here if invalid
  5. BOOL consistencyValid = [self validateConsistency:error];
  6. return (propertiesValid && consistencyValid);
  7. }
  8. - (BOOL)validateForUpdate:(NSError **)error
  9. {
  10. BOOL propertiesValid = [super validateForUpdate:error];
  11. // could stop here if invalid
  12. BOOL consistencyValid = [self validateConsistency:error];
  13. return (propertiesValid && consistencyValid);
  14. }
  15. - (BOOL)validateConsistency:(NSError **)error
  16. {
  17. static NSCalendar *gregorianCalendar;
  18. NSDate *myBirthday = [self birthday];
  19. if (myBirthday == nil) {
  20. return YES;
  21. }
  22. if ([[self hasDrivingLicense] boolValue] == NO) {
  23. return YES;
  24. }
  25. if (gregorianCalendar == nil) {
  26. gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
  27. }
  28. NSDateComponents *components = [gregorianCalendar components:NSCalendarUnitYear fromDate:myBirthday toDate:[NSDate date] options:0];
  29. NSInteger years = [components year];
  30. if (years >= 16) {
  31. return YES;
  32. }
  33. if (error == NULL) {
  34. //don't create an error if none was requested
  35. return NO;
  36. }
  37. NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
  38. NSString *drivingAgeErrorString = [myBundle localizedStringForKey:@"TooYoungToDriveError" value:@"Person is too young to have a driving license." table:@"PersonErrorStrings"];
  39. NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
  40. [userInfo setObject:drivingAgeErrorString forKey:NSLocalizedFailureReasonErrorKey];
  41. [userInfo setObject:self forKey:NSValidationObjectErrorKey];
  42. NSError *drivingAgeError = [NSError errorWithDomain:EMPLOYEE_ERROR_DOMAIN code:NSManagedObjectValidationError userInfo:userInfo];
  43. if (*error == nil) { // if there was no previous error, return the new error
  44. *error = drivingAgeError;
  45. } else { // if there was a previous error, combine it with the existing one
  46. *error = [self errorFromOriginalError:*error error:drivingAgeError];
  47. }
  48. return NO;
  49. }

Swift

  1. override func validateForInsert() throws {
  2. try super.validateForInsert()
  3. try validateConsistency()
  4. }
  5. override func validateForUpdate() throws {
  6. try super.validateForUpdate()
  7. try validateConsistency()
  8. }
  9. func validateConsistency() throws {
  10. guard let myBirthday = dateOfBirth as? Date else {
  11. let errString = "Person has no birth date set."
  12. let userInfo = [NSLocalizedFailureReasonErrorKey: errString, NSValidationObjectErrorKey: self] as [String : Any]
  13. throw NSError(domain: "EMPLOYEE_ERROR_DOMAIN", code: 1124, userInfo: userInfo)
  14. }
  15. if !hasDrivingLicense {
  16. return
  17. }
  18. let gregorianCalendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
  19. let components = gregorianCalendar.components(.Year, fromDate: myBirthday, toDate: Date(), options:.WrapComponents)
  20. if components.year >= 16 {
  21. return
  22. }
  23. let errString = "Person is too young to have a driving license."
  24. let userInfo = [NSLocalizedFailureReasonErrorKey: errString, NSValidationObjectErrorKey: self]
  25. let error = NSError(domain: "EMPLOYEE_ERROR_DOMAIN", code: 1123, userInfo: userInfo)
  26. throw error
  27. }

Combining Validation Errors

If there are multiple validation failures in a single operation, you create and return an NSError object with the code NSValidationMultipleErrorsError (for multiple errors error). 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. If the original error was already a multiple errors error, then the second error is added to it. Otherwise the two errors are combined together to create a new multiple errors error.

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

Objective-C

  1. - (NSError *)errorFromOriginalError:(NSError *)originalError error:(NSError*)secondError
  2. {
  3. NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
  4. NSMutableArray *errors = [NSMutableArray arrayWithObject:secondError];
  5. if ([originalError code] == NSValidationMultipleErrorsError) {
  6. [userInfo addEntriesFromDictionary:[originalError userInfo]];
  7. [errors addObjectsFromArray:[userInfo objectForKey:NSDetailedErrorsKey]];
  8. } else {
  9. [errors addObject:originalError];
  10. }
  11. [userInfo setObject:errors forKey:NSDetailedErrorsKey];
  12. return [NSError errorWithDomain:NSCocoaErrorDomain code:NSValidationMultipleErrorsError userInfo:userInfo];
  13. }

Swift

  1. func errorFromOriginalError(_ originalError: NSError, secondError: NSError) -> NSError {
  2. var userInfo = [String : Any]()
  3. var errors = [NSError]()
  4. if originalError.code == NSValidationMultipleErrorsError {
  5. for (k, v) in originalError.userInfo {
  6. guard let key = k as? String else { continue }
  7. userInfo.updateValue(v as AnyObject, forKey: key)
  8. }
  9. if let detailedErrors = userInfo[NSDetailedErrorsKey] as? [NSError] {
  10. errors = errors + detailedErrors
  11. }
  12. } else {
  13. errors.append(originalError)
  14. }
  15. userInfo[NSDetailedErrorsKey] = errors
  16. return NSError(domain: NSCocoaErrorDomain, code: NSValidationMultipleErrorsError, userInfo: userInfo)
  17. }