Search code examples
jsonswiftcore-datadecodable

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


I have the following JSON structure:

{
  "base": "USD",
  "date": "2020-04-24",
  "rates": {
    "CAD": 1.4049074074,
    "EUR": 0.9259259259
  }
}

I'd like to parse it to a class and store it in Core Data:

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

    @NSManaged public var base: String
    @NSManaged public var date: Date
    @NSManaged public var rates: [String:Double]

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

From what I read in order to store rates field in Core Data, I have to create another entity like this and create a one-to-many relationship:

class SubCurrencyRate: NSManagedObject, Decodable {
    enum CodingKeys: String, CodingKey {
        case currency
        case rate
    }

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

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

And the field rates in the CurrencyRate class would be: @NSManaged public var rates: [SubCurrencyRate]

The problem is I don't know how to parse the nested json with dynamic keys into these classes. How can it be done?

And how in general I can decode this part of JSON:

"rates": {
  "CAD": 1.4049074074,
  "EUR": 0.9259259259
}

into many SubCurrencyRate objects?


Solution

  • 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.