Search code examples
jsonswiftcore-datadecode

JSON Decoder not working when inserting data into CoreData in Swift


My app consists of a ProductFamily entity with a name attribute and an array of PartDetail dictionaries defined as a one-to-many relationship in CoreData. For each ProductFamily, I can have many PartDetail entities (PartNumbers) but for each PartDetail, it can only be associated with one ProductFamily. My example has 5 ProductFamilies, each with an array of 5 PartDetail dictionaries. I'm struggling to get my JSON decoder correct. It's not importing any data into CoreData. You can clone my sample project here:

https://github.com/jegrasso19/ProductFinder-Test2.git

A sample of my JSON data looks like this:

[
    {
        "Product Family 1": [
            {
                "partNumber": "160-9013-900",
                "orderable": true,
                "pnDescription": "Part Number Description"
            },
            {
                "partNumber": "160-9104-900",
                "orderable": true,
                "pnDescription": "Part Number Description"
            },
            {
                "partNumber": "160-9105-900",
                "orderable": false,
                "pnDescription": "Part Number Description"
            },
            {
                "partNumber": "160-9108-900",
                "orderable": true,
                "pnDescription": "Part Number Description"
            },
            {
                "partNumber": "160-9109-900",
                "orderable": true,
                "pnDescription": "Part Number Description"
            }
        ]
    },
    {
        "Product Family 2": [
            {
                "partNumber": "160-9113-900",
                "orderable": true,
                "pnDescription": "Part Number Description"
            },
            {
                "partNumber": "160-9114-900",
                "orderable": true,
                "pnDescription": "Part Number Description"
            },
            {
                "partNumber": "160-9115-900",
                "orderable": false,
                "pnDescription": "Part Number Description"
            },
            {
                "partNumber": "160-9116-900",
                "orderable": true,
                "pnDescription": "Part Number Description"
            },
            {
                "partNumber": "160-9201-900",
                "orderable": true,
                "pnDescription": "Part Number Description"
            }
        ]
    }
]

My ProductFamilyJSON Decoder file and ProductFamilyProperties looks like this:

import Foundation

struct ProductFamilyJSON: Decodable {

    // Struct that conforms with CodingKey so we can retrieve the product family name as a key
    //
    private struct JSONCodingKeys: CodingKey {
        var stringValue: String
        var intValue: Int?

        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        init?(intValue: Int) {
            self.init(stringValue: "\(intValue)")
            self.intValue = intValue
        }
    }
    // This is the dictionary that contains the JSON data
    // The key is the ProductFamily name, and the value is an array of PartDetailInfo.
    //
    private(set) var productFamilies = [ProductFamilyProperties]()
 
    init(from decoder: Decoder) throws {
        
        var rootContainer = try decoder.unkeyedContainer()
        let nestedProductFamilyContainer = try rootContainer.nestedContainer(keyedBy: JSONCodingKeys.self)
        
        // This is where my code fails. When decoding the JSON file, 
        // it never goes into the while loop.
        var productFamily = try ProductFamilyProperties(from: decoder)
        
        while !rootContainer.isAtEnd {
            
            let productFamilyKey = nestedProductFamilyContainer.allKeys.first!
            
            if var partNumberArrayContainer = try? nestedProductFamilyContainer.nestedUnkeyedContainer(forKey: productFamilyKey) {
                
                var partNumbers = Array<PartDetailInfo>()
                
                while !partNumberArrayContainer.isAtEnd {
                    
                    if let partNumber = try? partNumberArrayContainer.decode(PartDetailInfo.self) {
                        partNumbers.append(partNumber)
                    }
                }
                productFamily.code = UUID().uuidString
                productFamily.name = productFamilyKey.stringValue
                productFamily.partNumbers = partNumbers
                productFamilies.append(productFamily)
            }
        }
        print(productFamilies)
    }
}

import Foundation

struct ProductFamilyProperties : Decodable {

    var code: String
    var name: String
    var partNumbers: Array<PartDetailInfo>
    
    enum CodingKeys: String, CodingKey {
        case code
        case name
        case partNumbers
    }
    
    init(from decoder: Decoder) throws {
        
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let rawCode = try? values.decode(String.self, forKey: .code)
        let rawName = try? values.decode(String.self, forKey: .name)
        let rawPartNumbers = try? values.decode(Array<PartDetailInfo>.self, forKey: .partNumbers)
        
        guard let code = rawCode,
              let name = rawName,
              let partNumbers = rawPartNumbers
        else {
            throw myError.programError("Missing Data from Product Family")
        }
        
        self.code = code
        self.name = name
        self.partNumbers = partNumbers
    }

    var dictionaryValue: [String: Any] {
        [
            "code": code,
            "name": name,
            "partNumbers": partNumbers
        ]
    }
}

In my ProductFamilyJSON file, it seems to quit at defining the productFamily variable, which is based on my ProductFamilyProperties. This is apparently wrong but I don't know what it should be defined as. This is my first iOS app I'm trying to develop and learn from. I've spent a while learning CoreData and I've seen so many examples but very few use NSBatchInsertRequest and everyone seems to do this a little differently. I would appreciate some insight on getting this to work. Thanks.

Here is my CoreDataManager class, which contains the NSBatchInsertRequest for reference.

import Foundation
import CoreData

class CoreDataManager: ObservableObject {
    
    let persistentContainer: NSPersistentContainer
    
    static var shared = CoreDataManager()
    
    var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    
    private init() {
        
        persistentContainer = NSPersistentContainer(name: "ProductFinderTest")
        persistentContainer.loadPersistentStores { (description, error) in
            if let error = error {
                fatalError("Unable to initialize Core Data \(error)")
            }
        }
        let directories = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        print(directories[0])
    }
    
    func newTaskContext() -> NSManagedObjectContext {
        
        let taskContext = persistentContainer.newBackgroundContext()
        taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        taskContext.undoManager = nil
        return taskContext
    }
}

extension CoreDataManager {

    func fetchProductData() async throws {
        
        guard let url = Bundle.main.url(forResource: "ProductFamilies", withExtension: "json"),
            let jsonData = try? Data(contentsOf: url)
        else {
            throw myError.programError("Failed to receive valid response and/or Product Family data.")
        }
        do {
            let jsonDecoder = JSONDecoder()
            
            // ProductFamilyJSON uses this code
            let productFamilyJSON = try jsonDecoder.decode(ProductFamilyJSON.self, from: jsonData)
            let productFamilyList = productFamilyJSON.productFamilies
            
            print("Received \(productFamilyList.count) Product records.")
            print("Start importing product data to the store...")
            
            try await importProductData(from: productFamilyList)
            
            print("Finished importing product data.")
        } catch {
            throw myError.programError("Wrong Data Format for Product Families")
        }
    }

    private func importProductData(from productList: [ProductFamilyProperties]) async throws {
        guard !productList.isEmpty else { return }
        
        let taskContext = newTaskContext()

        taskContext.name = "importProductDataContext"
        taskContext.transactionAuthor = "importProductData"

        try await taskContext.perform {

            let batchInsertRequest = self.productListBatchInsertRequest(with: productList)
            if let fetchResult = try? taskContext.execute(batchInsertRequest),
               let batchInsertResult = fetchResult as? NSBatchInsertResult,
               let success = batchInsertResult.result as? Bool, success {
                return
            }
            else {
                throw myError.programError("Failed to execute ProductList batch import request.")
            }
        }
        print("Successfully imported Product data.")
    }

    private func productListBatchInsertRequest(with productList: [ProductFamilyProperties]) -> NSBatchInsertRequest {
        var index = 0
        let total = productList.count

        let batchInsertRequest = NSBatchInsertRequest(entity: ProductFamily.entity(), dictionaryHandler: { dictionary in
            guard index < total else { return true }
            
            dictionary.addEntries(from: productList[index].dictionaryValue)
            index += 1
            return false
        })
        return batchInsertRequest
    }

    func requestProductFamilies() -> NSFetchedResultsController<ProductFamily> {
        
        var fetchedResultsController: NSFetchedResultsController<ProductFamily>!
        
        let request: NSFetchRequest = ProductFamily.fetchProductFamilyRequest()
        request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        fetchedResultsController = NSFetchedResultsController(fetchRequest: request,
                                                                managedObjectContext: viewContext,
                                                                sectionNameKeyPath: nil,
                                                                cacheName: nil)
        try? fetchedResultsController.performFetch()
        
        return fetchedResultsController
    }
    
    func deleteProductData() async throws {
        
        let taskContext = self.newTaskContext()
        let fetchedResultsController = requestProductFamilies()
        try fetchedResultsController.performFetch()
        
        let productFamilies = (fetchedResultsController.fetchedObjects ?? []).map(ProductFamilyViewModel.init)
  
        guard !productFamilies.isEmpty else {
            print("ProductFamily database is empty.")
            return
        }
        let objectIDs = productFamilies.map { $0.objectId }

        print("Start deleting Product data from the store...")
        try await taskContext.perform {
            let batchDeleteRequest = NSBatchDeleteRequest(objectIDs: objectIDs)
            guard let fetchResult = try? taskContext.execute(batchDeleteRequest),
                  let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                  let success = batchDeleteResult.result as? Bool, success
            else {
                throw myError.programError("Failed to execute Product Family batch delete request.")
            }
        }
        print("Successfully deleted Product data.")
    }
}

Solution

  • The problem was with how I was initializing the productFamily variable. I needed to initialize it with the actual values instead of as an empty variable. I also needed to move the nestedProductFamilyContainer inside the while loop. Here is the correct ProductFamilyJSON decoder file. In addition, I changed the partNumber attribute in my ProductFamily entity from NSSet to Array, which allowed more flexibility.

    @vadian - I did remove the init(from:) and CodingKeys from ProductFamilyProperties as you suggested and it works just fine. Thanks for the input.

    import Foundation
    
    struct ProductFamilyJSON: Decodable {
    
        // Struct that conforms with CodingKey so we can retrieve the product family name as a key
        //
        private struct JSONCodingKeys: CodingKey {
            var stringValue: String
            var intValue: Int?
    
            init?(stringValue: String) {
                self.stringValue = stringValue
            }
    
            init?(intValue: Int) {
                self.init(stringValue: "\(intValue)")
                self.intValue = intValue
            }
        }
        // This is the dictionary that contains the JSON data
        // The key is the ProductFamily name, and the value is an array of PartDetailInfo.
        //
        private(set) var productFamilies = [ProductFamilyProperties]()
     
        init(from decoder: Decoder) throws {
    
            var rootContainer = try decoder.unkeyedContainer()
            
            while !rootContainer.isAtEnd {
                
                let nestedProductFamilyContainer = try rootContainer.nestedContainer(keyedBy: JSONCodingKeys.self)
                let productFamilyKey = nestedProductFamilyContainer.allKeys.first!
                
                if var partNumberArrayContainer = try? nestedProductFamilyContainer.nestedUnkeyedContainer(forKey: productFamilyKey) {
                    
                    var partNumbers = Array<PartDetailProperties>()
                    
                    while !partNumberArrayContainer.isAtEnd {
                    
                        if let partNumber = try? partNumberArrayContainer.decode(PartDetailProperties.self) {
                            partNumbers.append(partNumber)
                        }
                    }
                    let partNumbersSorted = partNumbers.sorted(by: { $0.partNumber < $1.partNumber })
                    
                    let productFamily = ProductFamilyProperties(code: UUID().uuidString, name: productFamilyKey.stringValue, partNumbers: partNumbersSorted)
                    productFamilies.append(productFamily)
                }
            }
            print(productFamilies)
        }
    }