Using Codable with Core Data and NSManagedObject
Published on: August 3, 2020If 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.