How to use swift 4 Codable in Core Data?
Asked Answered
S

4

58

Codable seems a very exciting feature. But I wonder how we can use it in Core Data? In particular, is it possible to directly encode/decode a JSON from/to a NSManagedObject?

I tried a very simple example:

enter image description here

and defined Foo myself:

import CoreData

@objc(Foo)
public class Foo: NSManagedObject, Codable {}

But when using it like this:

let json = """
{
    "name": "foo",
    "bars": [{
        "name": "bar1",
    }], [{
        "name": "bar2"
    }]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let foo = try! decoder.decode(Foo.self, from: json)
print(foo)

The compiler failed with this errror:

super.init isn't called on all paths before returning from initializer

and the target file was the file that defined Foo

I guess I probably did it wrong, since I didn't even pass a NSManagedObjectContext, but I have no idea where to stick it.

Does Core Data support Codable?

Sunbeam answered 9/6, 2017 at 5:45 Comment(1)
A good example which uses the accepted answer can be found hereRecorder
W
102

You can use the Codable interface with CoreData objects to encode and decode data, however it's not as automatic as when used with plain old swift objects. Here's how you can implement JSON Decoding directly with Core Data objects:

First, you make your object implement Codable. This interface must be defined on the object, and not in an extension. You can also define your Coding Keys in this class.

class MyManagedObject: NSManagedObject, Codable {
    @NSManaged var property: String?

    enum CodingKeys: String, CodingKey {
       case property = "json_key"
    }
}

Next, you can define the init method. This must also be defined in the class method because the init method is required by the Decodable protocol.

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

However, the proper initializer for use with managed objects is:

NSManagedObject.init(entity: NSEntityDescription, into context: NSManagedObjectContext)

So, the secret here is to use the userInfo dictionary to pass in the proper context object into the initializer. To do this, you'll need to extend the CodingUserInfoKey struct with a new key:

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

Now, you can just as the decoder for the context:

required convenience init(from decoder: Decoder) throws {

    guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
    guard let entity = NSEntityDescription.entity(forEntityName: "MyManagedObject", in: context) else { fatalError() }

    self.init(entity: entity, in: context)

    let container = decoder.container(keyedBy: CodingKeys.self)
    self.property = container.decodeIfPresent(String.self, forKey: .property)
}

Now, when you set up the decoding for Managed Objects, you'll need to pass along the proper context object:

let data = //raw json data in Data object
let context = persistentContainer.newBackgroundContext()
let decoder = JSONDecoder()
decoder.userInfo[.context] = context

_ = try decoder.decode(MyManagedObject.self, from: data) //we'll get the value from another context using a fetch request later...

try context.save() //make sure to save your data once decoding is complete

To encode data, you'll need to do something similar using the encode protocol function.

Wernick answered 24/10, 2017 at 17:53 Comment(14)
Great idea. Is there any way to initialize and then update existing objects this way? For example check if the id is already in CoreData. If it exists load the object and update, otherwise create new (as described above).Vacuole
Thanks for this, Saul!Udder
I'd add that since decoding values from JSON can throw, this code potentially allows objects to be inserted in the context even if JSON decoding encountered an error. You could catch this from the calling code and handle it by deleting the just-inserted-but-throwing object.Udder
Hey i followed the same steps as above but the context.hasChanges always gives me false even if the the managedobject has values after decoding. Since there are no changes context.save is not saving. I tried to call context.save directly, i passes with no error but database is empty. I also checked the pointer of the context that was passed to decoder and it is same. Any idea?Perfidy
I was getting Command failed due to signal: Abort trap: 6 error in Xcode9 until I properly adopt Encoder protocol like so: func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(property, forKey: .property) }Rembrandt
@Perfidy Did you manage to make it work? I have the same problem here, database is empty, context is not persisted.Thrifty
@Thrifty Actually i moved to Realm database as there was lot of boiler plate code with CoreData.Perfidy
This answer doesn't help if you want to update an existing object. It always creates new object and duplicates your existing records.Kowatch
Is there a way to do something like this but using Structs?Soil
Hi, I am encoding (in iOS app) and decoding (in watch kit extension), how did you manage to get the same context there?Cyndi
Hello I'm getting this errors first while creating an optional property inside the class Property cannot be marked @NSManaged because its type cannot be represented in Objective-C and than for the init line.Incorrect argument labels in call (have 'entity:in:', expected '_:using:')Thew
i am getting compiletime error "'NSManagedObjectContext?' is not convertible to 'NSManagedObjectContext'" at "guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError() }"Higgs
Say I don't want to necessarily save the data whenever I decode it, sometimes I just want to decode it and use it for other purpose, how to achieve that ? Because if I don't want to save the data, I don't need the context and won't pass it but then my code will crash because it will look for context in guard and reaches fatalError().Selie
If you're getting an error after implementing this that says "the given data did not contain a top-level value", you should try changing your entity class codegen to Manual/None and then choose Editor -> Create NSManagedObject Subclass and use the generated entity class as your codable objectTrafalgar
V
13

CoreData is its own persistence framework and, per its thorough documentation, you must use its designated initializers and follow a rather specific path to creating and storing objects with it.

You can still use Codable with it in limited ways just as you can use NSCoding, however.

One way is to decode an object (or a struct) with either of these protocols and transfer its properties into a new NSManagedObject instance you've created per Core Data's docs.

Another way (which is very common) is to use one of the protocols only for a non-standard object you want to store in a managed object's properties. By "non-standard", I mean anything thst doesn't conform to Core Data's standard attribute types as specified in your model. For example, NSColor can't be stored directly as a Managed Object property since it's not one of the basic attribute types CD supports. Instead, you can use NSKeyedArchiver to serialize the color into an NSData instance and store it as a Data property in the Managed Object. Reverse this process with NSKeyedUnarchiver. That's simplistic and there is a much better way to do this with Core Data (see Transient Attributes) but it illustrates my point.

You could also conceivably adopt Encodable (one of the two protocols that compose Codable - can you guess the name of the other?) to convert a Managed Object instance directly to JSON for sharing but you'd have to specify coding keys and your own custom encode implementation since it won't be auto-synthesized by the compiler with custom coding keys. In this case you'd want to specify only the keys (properties) you want to be included.

Hope this helps.

Voluptuous answered 9/6, 2017 at 14:20 Comment(19)
Thanks for the detailed explanation. I'm currently using the first approach as you mentioned. But I really hope NSManagedObject can conform to Codable by default, and there are methods like json = encoder.encode(foo) to encode it directly, and foo = decoder.decode(Foo.self, json, context) to decode directly. Hope to see it in an update or in the next major release.Sunbeam
I really wouldn't count on it. The ability to customize the encoding/decoding pretty much covers all the bases for data transfer between your app's store and the majority of the real-world cases with the JSON de/coder alone. Since the two approaches are mutually exclusive for app persistence (due to their radically different design approaches and use cases) there's somewhere around zero chance of such support. But hope springs eternal. ;-)Voluptuous
@JoshuaNozzi I totally disagree with this comment. You can change the mappings fairly easily and people use libraries for this specific approach. I wouldn't be surprised if support came in 2 or so iterations of iOS in the future. It would just require a tweak to the protocol to support population without initialization, or base level conformance with CoreData's current initialization interfaces and Codable enum code generation for CoreData models (which they already have code generation). The approaches aren't mutually exclusive and 99% of apps using core data are mapping JSON.Meson
@Meson To what are you referring? Custom store types? That’s a bit different to directly using Codable/Decodable directly on individual managed objects apart from the Core Data machinery.Voluptuous
@JoshuaNozzi I never referenced anything about custom store types.... This is a simple serialization/deserialization mapping of properties in Swift with the Codable Code-generated key values.Meson
@Meson Okay...? So what are you saying?Voluptuous
@JoshuaNozzi You specifically noted that the Codable protocol conflicts with CoreData's architecture per your comment here: "Since the two approaches are mutually exclusive for app persistence (due to their radically different design approaches and use cases) there's somewhere around zero chance of such support.". My notion is this is false. There is no conflict of serialization and deserialization with CoreData models. It's simply a lack of support for the property code generation and current requirement of an initializer for Decoder.Meson
@JoshuaNozzi give it time for the protocols to be adopted by NSManagedObject on a framework level or for the protocol itself to change an adopt support for non-initializer requirements and it should be straight forward to gain this support on NSManagedObjects.Meson
@JoshuaNozzi with the changes going into CoreData (per the last WWDC session discussion), it wouldn't surprise me if we received this as a nice update in the near future. Regardless, I don't think anything backs up the notion that this conflicts with any architecture for CoreData and shouldn't be expected in a future update. If anything, I highly recommend creating a radar for it so we see it in iOS 12.Meson
@Meson You misinterpreted my meaning. Using the two together for storage would be pointless. You can obviously add support to get A serialized version of your managed objects for use elsewhere but what would be the point in using Core Data if you were using Codable for the storage aspect at the managed object level as well?Voluptuous
Let us continue this discussion in chat.Meson
since it won't be auto-synthesized by the compiler with custom coding keys why not?Recorder
@Honey How could it? The auto-synthesizing is only possible because it does so for all known properties (the keys, the coding, and decoding behavior). If you override any part of this (the keys, the encoding, or the decoding), it affects some or all the other parts, depending on whether you’re compliant with coding, decoding, or both, and whether you overrode the auto-synth’d keys.Voluptuous
Thanks 1. Imagine this: public class CityEntity: NSManagedObject, Codable {}; extension CityEntity { @nonobjc public class func fetchRequest() -> NSFetchRequest<CityEntity> { return NSFetchRequest<CityEntity>(entityName: "CityEntity") } @NSManaged public var name: String? @NSManaged public var population: Int16 }; so you're saying none of the public vars are considered known properties? ok. So what kind of a property are they? I'm just trying to get my jargon right. 2. Also you used the word override. What is being overrode here?Recorder
@Honey You really need to post this as its own question. Especially since this involves a Core Data managed object, which has its own requirements and behaviors (doubly so when using with Codable). I admit this is a shady area for me, as I've found Core Data to be a terrible fit for most of my professional work for various reasons. As to my terminology, don't read so much into it. Core Data aside, the compiler knows what properties you define on classes and structs. As to "override", I mean the compiler's auto-synth behavior is overridden the moment you specify code for your own keys.Voluptuous
@Honey There are far too many subtleties for it to be fair to expect any kind of detailed and enlightening answer in comments, which is why I started by saying you should post your own situation as its own question so that you can specify exactly what it is you're trying to accomplish / understand in your exact situation. This gives people the ability to provide a full answer with no character limits and with proper formatting. An extended discussion in comments is not only counter to SO usage, but is tantamount to writing an app via text message. ;-)Voluptuous
Also, as an aside, I find it hilarious as a gay man that your user name makes me sound like I’m condescending you by calling you @Honey. “Honey, you really need to...” :-DVoluptuous
I'm sorry are you gay or you're thinking I am? :)Recorder
LOL, I was referring to myself. The “honey” thing is a gay thing I tend to avoid unless I’m being funny.Voluptuous
F
7

Swift 4.2:

Following casademora's solution,

guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError() }

should be

guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }.

This prevents errors that Xcode falsely recognizes as array slice problems.

Edit: Use implicitly unwrapped optionals to remove the need to force unwrap .context every time it is being used.

Fiduciary answered 24/7, 2018 at 22:0 Comment(4)
I would rather make the static constant (.context) force unwrapped at the definition instead of sprinkling it all over the source like this.Wernick
@Wernick this is the same as your answer, just for swift 4.2 (EDIT: I see what you mean. Implicitly unwrapped optionals :-). ).Fiduciary
Yeah, I’m aware of the difference. I’m just suggesting putting the unwrapping on the constant (in one place) as opposed to the userInfo accessor (potentially everywhere)Wernick
Hi, I am encoding (in iOS app) and decoding (in watch kit extension), how did you manage to get the same context there?Cyndi
E
6

As an alternative for those who would like to make use of XCode's modern approach to NSManagedObject file generation, I have created a DecoderWrapper class to expose a Decoder object which I then use within my object which conforms to a JSONDecoding protocol:

class DecoderWrapper: Decodable {

    let decoder:Decoder

    required init(from decoder:Decoder) throws {

        self.decoder = decoder
    }
}

protocol JSONDecoding {
     func decodeWith(_ decoder: Decoder) throws
}

extension JSONDecoding where Self:NSManagedObject {

    func decode(json:[String:Any]) throws {

        let data = try JSONSerialization.data(withJSONObject: json, options: [])
        let wrapper = try JSONDecoder().decode(DecoderWrapper.self, from: data)
        try decodeWith(wrapper.decoder)
    }
}

extension MyCoreDataClass: JSONDecoding {

    enum CodingKeys: String, CodingKey {
        case name // For example
    }

    func decodeWith(_ decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try container.decode(String.self, forKey: .name)
    }
}

This is probably only useful for models without any non-optional attributes, but it solves my problem of wanting to use Decodable but also manage relationships and persistence with Core Data without having to manually create all my classes / properties.

Edit: Example of it in use

If I have a json object:

let myjson = [ "name" : "Something" ]

I create the object in Core Data (force cast here for brevity):

let myObject = NSEntityDescription.insertNewObject(forEntityName: "MyCoreDataClass", into: myContext) as! MyCoreDataClass

And I use the extension to have the object decode the json:

do {
    try myObject.decode(json: myjson)
}
catch {
    // handle any error
}

Now myObject.name is "Something"

Edp answered 5/2, 2019 at 18:30 Comment(2)
If we have a Custom object like @NSManaged public var products: NSSet?. How will we decode this object.Sanfo
You can cast it to a regular set which is codableBurgoyne

© 2022 - 2024 — McMap. All rights reserved.