swiftcore-datansobjectcoremlnscoding

How to unarchive MLMultiArray with NSKeyedUnarchiver?


I want to store MLMultiArray data predicted from CoreML into local file, because Core Data does not support for this type, I tried out NSKeyedUnarchiver, as MLMultiArray conforms to NSObject.

However, this Object can be encode & save successfully but can not be decode correctly.

error is: Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'embedding' was of unexpected class 'MLMultiArray' (0x1e35d84b8) [/System/Library/Frameworks/CoreML.framework].
Allowed classes are:
 {(
    "'CoreMLApp.Embedding' (0x1002a53f8) [/private/var/containers/Bundle/Application/C86A850C-27F6-43CB-82AF-9223/CoreMLApp.app]",
    "'NSArray' (0x1e3380b50) [/System/Library/Frameworks/CoreFoundation.framework]"
)}" UserInfo={NSDebugDescription=value for key 'embedding' was of unexpected class 'MLMultiArray' (0x1e35d84b8)

Below is my Object:


class Embedding: NSObject, NSSecureCoding {
    static var supportsSecureCoding: Bool = true
  
    var id: String?
    var embedding: MLMultiArray?
    
    init(id: String, embedding: MLMultiArray) {
        self.id = id
        self.embedding = embedding
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(self.id, forKey: "id")
        aCoder.encode(self.embedding, forKey: "embedding")
    }
    
    required init?(coder aDecoder: NSCoder) {
        self.id = aDecoder.decodeObject(forKey: "id") as? String
        self.embedding = aDecoder.decodeObject(forKey: "embedding") as? MLMultiArray
    }
}

The encode/decode part:

    private func saveEmbeddingsData(embeddings: [Embedding], fileName: String) -> Bool {
        let filePath = self.getDocumentsDirectory().appendingPathComponent(fileName)
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: embeddings, requiringSecureCoding: true)
            try data.write(to: filePath)
            return true
        } catch {
            print("error is: \(error.localizedDescription)")
        }
        return false
    }

    private func loadEmbeddingsData(fileName: String) -> [Embedding]? {
        let filePath = self.getDocumentsDirectory().appendingPathComponent(fileName)
        do {
            let data = try Data(contentsOf: filePath)
            let embeddings = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClasses: [Embedding.self], from: data) as? [Embedding]
            return embeddings
        } catch {
            print("error is: \(String(describing: error))")
        }
        return nil
    }
    
    private func getDocumentsDirectory() -> URL {
        let arrayPaths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return arrayPaths[0]
    }

I've tried with NSCoding rather than NSSecureCoding, but all NSKeyedUnarchiver.unarchiveXXX methods output errors that require the data to conform NSSecureCoding.


Solution

  • NSSecureCoding and NSKeyedUnarchiver require that you specify the types of classes you are expecting. The whole idea is to avoid someone sneaking in unexpected, crafty values that could compromise the security of your code.

    Since you archive an array of Embedding, the archive also contains values of type MLMultiArray. This means that when you unarchive the data you must specify that you expect values of type, NSArray, Embedding, and MLMultiArray.

    Since you are using NSKeyedUnarchiver.unarchivedArrayOfObjects you don't need to explicitly specify NSArray. But you do need to pass [Embedding.self, MLMultiArray.self] to the classes parameter.

    The error message is the big hint.

    Allowed classes are:
     {(
        "'CoreMLApp.Embedding' (0x1002a53f8) [/private/var/containers/Bundle/Application/C86A850C-27F6-43CB-82AF-9223/CoreMLApp.app]",
        "'NSArray' (0x1e3380b50) [/System/Library/Frameworks/CoreFoundation.framework]"
    )}" UserInfo={NSDebugDescription=value for key 'embedding' was of unexpected class 'MLMultiArray' (0x1e35d84b8)
    

    This is telling you that only Embedding and NSArray are allowed since that is all you told NSKeyedUnarchiver.unarchivedArrayOfObjects to expect. And the error tells you that it found MLMultiArray in the archive but it was not in the list of allowed classes.

    This is resolved by changing the line:

    let embeddings = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClasses: [Embedding.self], from: data) as? [Embedding]
    

    to:

    let embeddings = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClasses: [Embedding.self, MLMultiArray.self], from: data) as? [Embedding]
    

    To me, the biggest issue with NSSecureCoding is that it breaks encapsulation. Your code to unarchive needs to know the internal details of the Embedding class. One solution to this may be to add helper methods to the Embedding class to do the archiving and unarchiving so other code doesn't need to know what classes need to be specified.