Overview

Core Data migrations update the on-disk store to match a new model version without discarding user data. Lightweight migration covers the common additive changes automatically. Custom mapping models handle structural changes that Core Data cannot infer. Versioning discipline and CI fixture tests prevent the scenario every iOS developer dreads: a released app that crashes on launch because the stored schema is incompatible. For the stack setup that enables migrations, see core-data-stack; for concurrency during migration, see core-data-concurrency.

Enable lightweight migration on the store description

Set both flags on the store description before loading. Core Data will infer a mapping model for any supported change automatically.

let description = container.persistentStoreDescriptions.first!
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true

Lightweight migration runs synchronously during loadPersistentStores. For large stores, move loading to a background thread; see core-data-stack for the async loading pattern.

Know what lightweight migration can and cannot handle

Lightweight migration supports: adding an attribute, removing an attribute, making an optional attribute required with a default, renaming an entity or attribute (with a renaming identifier), adding a relationship, and changing a relationship cardinality.

Lightweight migration does not support: splitting one entity into two, merging two entities into one, transforming an attribute’s type, deriving a new attribute’s value from multiple existing attributes, or reordering a to-ordered relationship.

When you need any of the unsupported changes, write a custom mapping model.

Version the model file on every structural change

Add a new model version in Xcode (Editor > Add Model Version) for every change you ship. Never edit the current version in place after any user has installed the app.

Model.xcdatamodeld/
  Model.xcdatamodel        -- v1, never edit
  Model v2.xcdatamodel     -- additive changes
  Model v3.xcdatamodel     -- current

Set the “Current Version” in the xcdatamodeld inspector to the latest version. Core Data reads this to know the target schema.

Write custom mapping models for structural changes

When lightweight inference fails, create a new Mapping Model file (File > New > Mapping Model). Map each source entity to its target entity. For value transformations, subclass NSEntityMigrationPolicy and override createDestinationInstances(forSource:in:manager:).

final class PersonToContactMigrationPolicy: NSEntityMigrationPolicy {
  override func createDestinationInstances(
    forSource source: NSManagedObject,
    in mapping: NSEntityMapping,
    manager: NSMigrationManager
  ) throws {
    let destination = NSEntityDescription.insertNewObject(
      forEntityName: "Contact",
      into: manager.destinationContext
    )
    destination.setValue(source.value(forKey: "firstName"), forKey: "givenName")
    destination.setValue(source.value(forKey: "lastName"), forKey: "familyName")
    let phone = (source.value(forKey: "phone") as? String)?
      .trimmingCharacters(in: .whitespaces)
    destination.setValue(phone?.isEmpty == false ? phone : nil, forKey: "phone")
    manager.associate(sourceInstance: source, withDestinationInstance: destination, for: mapping)
  }
}

Set the custom policy class name in the mapping model inspector under “Custom Policy.”

Test migrations in CI against a fixture store

Migrations that work in development can fail on edge-case data in production. Commit a binary fixture store for each released schema version and run a migration test on CI.

func testV2ToV3Migration() throws {
  let sourceURL = Bundle(for: Self.self)
    .url(forResource: "ModelV2", withExtension: "sqlite")!
  let tempURL = FileManager.default.temporaryDirectory
    .appendingPathComponent(UUID().uuidString + ".sqlite")
  try FileManager.default.copyItem(at: sourceURL, to: tempURL)
 
  let container = NSPersistentContainer(name: "Model")
  container.persistentStoreDescriptions.first!.url = tempURL
  container.persistentStoreDescriptions.first!.shouldMigrateStoreAutomatically = true
  container.persistentStoreDescriptions.first!.shouldInferMappingModelAutomatically = true
 
  var loadError: Error?
  container.loadPersistentStores { _, error in loadError = error }
  XCTAssertNil(loadError, "Migration should succeed without error")
 
  let ctx = container.viewContext
  let count = try ctx.count(for: NSFetchRequest(entityName: "Contact"))
  XCTAssertGreaterThan(count, 0, "Migrated contacts should exist")
}

Generate fixture stores by running the app against a known dataset and copying the .sqlite file from the simulator’s Documents directory.

Stage multi-version migrations step by step

Core Data can migrate across multiple versions in sequence (v1 to v3 via v2), but each hop must have an explicit mapping or be lightweight-inferrable. Document the migration chain and test each hop.

  • Keep mapping models named ModelV1toV2.xcmappingmodel for clarity.
  • Never skip a version; users on old versions must always have a valid migration path.
  • If a version is so old that supporting it is impractical, ship a “data recovery” flow that exports data before deleting and recreating the store.