Using Codable with Core Data and NSManagedObject

Published by donnywals on

If you've ever wanted to decode a bunch of JSON data into NSManagedObject instances you've probably noticed that this isn't a straightforward exercise. With plain structs, you can conform your struct to Codable and you convert the struct from and to JSON data automatically.

For an NSManagedObject subclass it's not that easy.

If your Core Data data model is configured to automatically generate your entity class definitions for you (which is the default), you may have tried to write the following code to conform your managed object to Decodable:

extension MyManagedObject: Decodable { }

If you do this, the compiler will tell you that it can't synthesize an implementation for init(from:) for a class that's defined in a different file. Xcode will offer you some suggestions like adding an initializer, marking it as convenience and eventually the errors will point you towards making your init required too, resulting in something like the following:

extension MyManagedObject: Decodable {
  required convenience public init(from decoder: Decoder) throws {
  }
}

Once you've written this you'll find that Xcode still isn't happy and that it presents you with the following error:

'required' initializer must be declared directly in class 'MyManagedObject' (not in an extension)

In this week's post, you will learn how you can manually define your managed object subclass and add support for Swift's JSON decoding and encoding features by conforming your managed object to Decodable and Encodable. First, I'll explain how you can tweak automatic class generation and define your managed object subclasses manually while still generating the definition for all of your entity's properties.

After that, I'll show you how to conform your managed object to Decodable, and lastly, we'll add conformance for Encodable as well to make your managed object conform to the Codable protocol (which is a combined protocol of Decodable and Encodable).

Tweaking your entity's code generation

Since we need to define our managed object subclass ourselves to add support for Codable, you need to make some changes to how Xcode generates code for you.

Open your xcdatamodeld file and select the entity that you want to manually define the managed object subclass for. In the sidebar on the right, activate the Data model inspector and set the Codegen dropdown to Category/Extension. Make sure that you set Module to Current product module and that Name is set to the name of the managed object subclass that you will define. Usually, this class name mirrors the name of your entity (but it doesn't have to).

After setting up your data model, you can define your subclasses. Since Xcode will generate an extension that contains all of the managed properties for your entity, you only have to define the classes that Xcode should extend:

class TodoItem: NSManagedObject {
}

class TodoCompletion: NSManagedObject {
}

Once you've defined your managed object subclasses, Xcode generates extensions for these classes that contain all of your managed properties while giving you the ability to add the required initializers for the Decodable and Encodable protocols.

Let's add conformance for Decodable first.

Conforming an NSManagedObject to Decodable

The Decodable protocol is used to convert JSON data into Swift objects. When your objects are relatively simple and closely mirror the structure of your JSON, you can conform the object to Decodable and the Swift compiler generates all the required decoding code for you.

Unfortunately, Swift can't generate this code for you when you want to make your managed object conform to Decodable.

Because Swift can't generate the required code, we need to define the init(from:) initializer ourselves. We also need to define the CodingKeys object that defines the JSON keys that we want to use when decoding JSON data. Adding the initializer and CodingKeys for the objects from the previous section looks as follows:

class TodoCompletion: NSManagedObject, Decodable {
  enum CodingKeys: CodingKey {
    case completionDate
  }

  required convenience init(from decoder: Decoder) throws {
  }
}

class TodoItem: NSManagedObject, Decodable {
  enum CodingKeys: CodingKey {
    case id, label, completions
  }

  required convenience init(from decoder: Decoder) throws {
  }
}

Before I get to the decoding part, we need to talk about managed objects a little bit more.

Managed objects are always associated with a managed object context. When you want to create an instance of a managed object you must pass a managed object context to the initializer.

When you're initializing your managed object with init(from:) you can't pass the managed object context along to the initializer directly. And since Xcode will complain if you don't call self.init from within your convenience initializer, we need a way to make a managed object context available within init(from:) so we can properly initialize the managed object.

This can be achieved through JSONDecoder's userInfo dictionary. I'll show you how to do this first, and then I'll show you what this means for the initializer of TodoItem from the code snippet I just showed you. After that, I will show you what TodoCompletion ends up looking like.

Since all keys in JSONDecoder's userInfo must be of type CodingUserInfoKey we need to extend CodingUserInfoKey first to create a managed object context key:

extension CodingUserInfoKey {
  static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
}

We can use this key to set and get a managed object context from the userInfo dictionary. Now let's create a JSONDecoder and set its userInfo dictionary:

let decoder = JSONDecoder()
decoder.userInfo[CodingUserInfoKey.managedObjectContext] = myPersistentContainer.viewContext

When we use this instance of JSONDecoder to decode data, the userInfo dictionary is available within the initializer of the object we're decoding to. Let's see how this works:

enum DecoderConfigurationError: Error {
  case missingManagedObjectContext
}

class TodoItem: NSManagedObject, Decodable {
  enum CodingKeys: CodingKey {
    case id, label, completions
 }

  required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
      throw DecoderConfigurationError.missingManagedObjectContext
    }

    self.init(context: context)

    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decode(Int64.self, forKey: .id)
    self.label = try container.decode(String.self, forKey: .label)
    self.completions = try container.decode(Set<TodoCompletion>.self, forKey: .completions) as NSSet
  }
}

In the initializer for TodoItem I try to extract the object at CodingUserInfoKey.managedObjectContext from the Decoder's userInfo dictionary and I try to cast it to an NSManagedObjectContext. If this fails I throw an error that I've defined myself because we can't proceed without a managed object context.

After that, I call self.init(context: context) to initialize the TodoItem and associate it with a managed object context.

The last step is to decode the object as you normally would by grabbing a container that's keyed by CodingKeys.self and decoding all relevant properties into the correct types.

Note that Core Data still uses Objective-C under the hood so you might have to cast some Swift types to their Objective-C counterparts like I had to with my Set<TodoCompletion>.

For completion, this is what the full class definition for TodoCompletion would look like:

class TodoCompletion: NSManagedObject, Decodable {
  enum CodingKeys: CodingKey {
    case completionDate
  }

  required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
      throw DecoderConfigurationError.missingManagedObjectContext
    }

    self.init(context: context)

    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.completionDate = try container.decode(Date.self, forKey: .completionDate)
  }
}

This code shouldn't look surprising; it's basically the same as the code for TodoItem. Note that the decoder that's used to decode the TodoItem is also used to decode TodoCompletion which means that it also has the managed object context in its userInfo dictionary.

If you want to test this code, you can use the following JSON as a starting point:

[
  {
    "id": 0,
    "label": "Item 0",
    "completions": []
  },
  {
    "id": 1,
    "label": "Item 1",
    "completions": [
      {
        "completionDate": 767645378
      }
    ]
  }
]

Unfortunately, it takes quite a bunch of code to make Decodable work with managed objects, but the final solution is something I'm not too unhappy with. I like how easy it is to use once set up properly.

Adding support for Encodable to an NSManagedObject

While we had to do a bunch of custom work to add support for Decodable to our managed objects, adding support for Encodable is far less involved. All we need to do is define encode(to:) for the objects that need Encodable support:

class TodoItem: NSManagedObject, Codable {
  enum CodingKeys: CodingKey {
    case id, label, completions
  }

  required convenience init(from decoder: Decoder) throws {
    // unchanged implementation
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
    try container.encode(label, forKey: .label)
    try container.encode(completions as! Set<TodoCompletion>, forKey: .completions)
  }
}

Note that I had to convert completions (which is an NSSet) to a Set<TodoCompletion> explicitly. The reason for this is that NSSet isn't Encodable but Set<TodoCompletion> is.

For completion, this is what TodoCompletion looks like with Encodable support:

class TodoCompletion: NSManagedObject, Codable {
  enum CodingKeys: CodingKey {
    case completionDate
  }

  required convenience init(from decoder: Decoder) throws {
    // unchanged implementation
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(completionDate, forKey: .completionDate)
  }
}

Note that there is nothing special that I had to do to conform my managed object to Encodable compared to a normal manual Encodable implementation.

In Summary

In this week's post, you learned how you can add support for Codable to your managed objects by changing Xcode's default code generation for Core Data entities, allowing you to write your own class definitions. You also saw how you can associate a managed object context with a JSONDecoder through its userInfo dictionary, allowing you to decode your managed objects directly from JSON without any extra steps. To wrap up, you saw how to add Encodable support, making your managed object conform to Codable rather than just Decodable.

If you have any questions about this post or if you have feedback for me, don't hesitate to shoot me a message on Twitter.


Practical Combine

Learn everything you need to know about Combine and how you can use it in your projects with my new book Practical Combine. You'll get thirteen chapters, a Playground and a handful of sample projects to help you get up and running with Combine as soon as possible.

The book is available as a digital download for just $24.99!

Get Practical Combine

Receive weekly updates about my posts