How to Store JSON Decodable Values into Coredata Using Swift

Save complex JSON to Core Data in Swift

It's quite easy to decode the JSON directly into Core Data with Decodable

  • First of all create extensions of CodingUserInfoKey and JSONDecoder to be able to pass the managed object context

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

    extension JSONDecoder {
    convenience init(context: NSManagedObjectContext) {
    self.init()
    self.userInfo[.context] = context
    }
    }
  • Add conformance to Decodable in both classes

    class Name: NSManagedObject, Decodable {

    class ParamDownload: NSManagedObject, Decodable {
  • In the class Name (not the extension) add

    private enum CodingKeys: String, CodingKey { case name, iin, isOn }

    required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("NSManagedObjectContext is missing") }
    let entity = NSEntityDescription.entity(forEntityName: "Name", in: context)!
    self.init(entity: entity, insertInto: context)
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = try values.decode(String.self, forKey: .name)
    iin = try values.decode(String.self.self, forKey: .iin)
    isOn = try values.decode(Bool.self.self, forKey: .isOn)
    }
  • In the class ParamDownload (not the extension) add

    private enum CodingKeys: String, CodingKey { case code = "Code", desc = "Desc", names = "iinData" }

    required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("NSManagedObjectContext is missing") }
    let entity = NSEntityDescription.entity(forEntityName: "ParamDownload", in: context)!
    self.init(entity: entity, insertInto: context)
    let values = try decoder.container(keyedBy: CodingKeys.self)
    code = try values.decode(String.self, forKey: .code)
    desc = try values.decode(String.self, forKey: .desc)
    names = try values.decode(Set<Name>.self, forKey: .names)
    names.forEach { $0.pd = self }
    }

To decode the JSON create the Decoder with the convenience initializer

let decoder = JSONDecoder(context: CoreDataStack.sharedInstance.persistentContainer.viewContext)

the init methods handle the relationships.

I recommend to declare the Core Data attributes as much non-optional as possible.

If an attribute must be remain optional replace decode with decodeIfPresent.

Decoding JSON nested dictionary using Decodable and storing it using Core Data

It's not necessary that SubCurrencyRate conforms to Decodable but you have to declare the reverse relationship (also in the data model!).

class SubCurrencyRate: NSManagedObject {

@NSManaged public var currency: String
@NSManaged public var rate: Double
@NSManaged public var currencyRate: CurrencyRate

To be able to parse JSON directly in Core Data declare two extensions

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

extension JSONDecoder {
convenience init(context: NSManagedObjectContext) {
self.init()
self.userInfo[.context] = context
}
}

In CurrencyRate you have to declare the to-many relationship as Set<SubCurrencyRate>, in init(from decoder decode the rates dictionary and map it to SubCurrencyRate instances. If the relationships are defined correctly in the model then the property as well as the reverse relationship is populated automatically.

class CurrencyRate: NSManagedObject, Decodable {
enum CodingKeys: String, CodingKey { case base, date, rates }

@NSManaged public var base: String
@NSManaged public var date: Date
@NSManaged public var rates: Set<SubCurrencyRate>

required convenience init(from decoder: Decoder) throws {

guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("Error: No object context!") }
let entity = NSEntityDescription.entity(forEntityName: "CurrencyRate", in: context)!
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
base = try values.decode(String.self, forKey: .base)
date = try values.decode(Date.self, forKey: .date)
let ratesData = try values.decode([String:Double].self, forKey: .rates)
for (key, value) in ratesData {
let subCurrencyRate = SubCurrencyRate(context: context)
subCurrencyRate.currency = key
subCurrencyRate.rate = value
subCurrencyRate.currencyRate = self
}
}
}

To decode the JSON you have to use the JSONDecoder(context: API in the extension passing the managed object context and to decode date properly you have to add an appropriate dateDecodingStrategy.

Save to CoreData array of strings with Codable

You need an inverse relationship to Movie in Genre. Add this

@NSManaged var movie: Movie?

and establish the connection in the model file.

Then decode an array of strings, map it to Genre instances and assign self to that relationship at the end of the init method

let genreData = try container.decodeIfPresent([String].self, forKey: .genres) ?? []
let genreArray = genreData.map { name in
let genre = Genre(context: manageObjContext)
genre.name = name
genre.movie = self
return genre
}
self.genres = Set(genreArray)

Consider to use a to-many relationship from Genre to Movie as well because otherwise you will have a lot of Genre instances with the same name. And consider also to reduce the optionals in the Core Data classes. It seems that the JSON source provides always all fields. You can get rid of a lot of redundant code.

How to use swift 4 Codable in Core Data?

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.



Related Topics



Leave a reply



Submit