`cannotDecodeObjectOfClassName` not invoked in `NSKeyedArchiverDelegate`

I am trying to catch the `NSKeyedUnarchiver` unarchiving exception `NSInvalidUnarchiveOperationException` where an unknown class is being decoded securely via `NSSecureCoding` protocol.


The solution I am using is based on a related [NSKeyedUnarchiverDelegate SO post][SONSKeyedUnarchiverDelegate] by implementing the delegate protocol `NSKeyedUnarchiverDelegate` so I can listen and respond to the exception via `unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:)`. However, this delegate method doesn't seem to be called when unknown class is encountered during decoding.


Here's the code snippet that I use for securely unarchiving an array object.

    func securelyUnarchiveArrayOfCustomObject(from url: URL, for key: String) -> [MyCustomClass]? {
        guard let data = try? Data(contentsOf: url) else {
            os_log("Unable to locate data at given url.path: %@", log: OSLog.default, type: .error, url.path)
            return nil
        }

        let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
        let delegate = UnarchiverDelegate()    // Prevents `NSInvalidUnarchiveOperationException` crash
        unarchiver.delegate = delegate
        unarchiver.requiresSecureCoding = true   // Prevents object substitution attack

        let allowedClasses = [NSArray.self] // Will decode without problem if using [NSArray.self, MyCustomClass.self]
        let decodedObject = unarchiver.decodeObject(of: allowedClasses, forKey: key)
        let images = decodedObject as! [ImageWithCaption]?
        unarchiver.finishDecoding()

        return images
    }


where my `UnarchiverDelegate` is implemented just like in the original [NSKeyedUnarchiverDelegate SO post][SONSKeyedUnarchiverDelegate] I pointed to. Under my setup, `decodeObject(of: allowedClasses, forKey: key)` doesn't throw an exception, but instead raises a runtime exception:


    'NSInvalidUnarchiveOperationException', reason:
    'value for key 'NS.objects' was of unexpected class
    'MyCustomClassProject.MyCustomClass'. Allowed classes are '{(
        NSArray
    )}'.'


This is supposedly just the kind of exception that `NSKeyedUnarchiverDelegate`'s `unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:)` should be invoked, based on [its documentation][AppleDocDelegate]:


> Informs the delegate that the class with a given name is not available during decoding.


But in my case, this method isn't invoked with the snippet above (even though other delegate methods, like `unarchiverWillFinish(_:)` or `unarchiver(_:didDecode:)` are invoked normally when decoding doesn't encounter a problem.

Unlike in the original post, I cannot use class function like `decodeTopLevelObjectForKey` where I can handle exception with `try?`, because I need to support secure encoding and decoding with `NSSecureCoding` protocol, like discussed [here][SONSSecureCodingCustomCollection]. This forces me to use `decodeObject(of:forKey)`, which doesn't throw any exception that I can handle, and, again, it doesn't inform my delegate before throwing runtime exception that leads to app crash either.

Under what scenarios is the delegate method `unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:)` actually invoked? How can I listen and react to the `NSInvalidUnarchiveOperationException` under my `NSSecureCoding` setup so I can avoid runtime crash when the decoding isn't successful?


[SONSKeyedUnarchiverDelegate]: http://stackoverflow.com/questions/32904811/swift-only-way-to-prevent-nskeyedunarchiver-decodeobject-crash

[SONSSecureCodingCustomCollection]: http://stackoverflow.com/questions/24376746/nssecurecoding-trouble-with-collections-of-custom-class

[AppleDocDelegate]: https://developer.apple.com/reference/foundation/nskeyedunarchiverdelegate/1409948-unarchiver

Accepted Reply

This is extremely poorly documented (for Obj-C) and it's worse when you try to do it in Swift.


At the top level, you need to use the Swift-only "decodeTopLevelObject(of:forKey:)", which is actually two overloaded instance methods, one for a single type (this method is generic), and one for an array of types (which is the one you seem to need). If you don't use one of these functions, errors are not thrown all the back up to the top level. These functions are not exactly part of the Cocoa SDK, but are defined in the special version of Foundation that Swift uses instead.


The other problem is that when you're decoding an array securely (either at the top level, or at a lower level where you use "decodeObject(of:forKey:)"), you can't just specify [NSArray.self] for the types. Your array must also list all the types of the objects in the array. In Swift, where arrays tend to be homogeneous, that usually means you specify something like [NSArray.self, MyElementType.self]. This is non-obvious, obviously, but you just have to know to do it. 😉

Replies

This is extremely poorly documented (for Obj-C) and it's worse when you try to do it in Swift.


At the top level, you need to use the Swift-only "decodeTopLevelObject(of:forKey:)", which is actually two overloaded instance methods, one for a single type (this method is generic), and one for an array of types (which is the one you seem to need). If you don't use one of these functions, errors are not thrown all the back up to the top level. These functions are not exactly part of the Cocoa SDK, but are defined in the special version of Foundation that Swift uses instead.


The other problem is that when you're decoding an array securely (either at the top level, or at a lower level where you use "decodeObject(of:forKey:)"), you can't just specify [NSArray.self] for the types. Your array must also list all the types of the objects in the array. In Swift, where arrays tend to be homogeneous, that usually means you specify something like [NSArray.self, MyElementType.self]. This is non-obvious, obviously, but you just have to know to do it. 😉

Hi QuinceyMorris, thank you for the reply.


I pasted the code from my own StackOverflow post, and didn't realize that my comments have been chopped off. I actually already knew that I need to include the types of the object itself, not just [NSArray.self]. My updated code above is:


let allowedClasses = [NSArray.self] // Will decode without problem if using [NSArray.self, MyCustomClass.self]


I intentionally do this because I am trying to handle the scenario where the class being decoded is unknown, to make my decoding process more robust. Yes,

decodeTopLevelObject(of:forKey:)

will allow me to handle this since it throws an error, unlike

decodeObject(of: allowedClasses, forKey: key)

which doesn't throw an error if decoding goes wrong. However, Apple [doc on NSSecureCoding](https://developer.apple.com/reference/foundation/nssecurecoding) makes clear that I must use the latter to conform to `NSSecureCoding`. So that's why I put my attention on trying to make the delegate to work, and I'm wondering why my delegate doesn't get notified via `unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:)` when an unknown class is encountered in `decodeObject(of: allowedClasses, forKey: key)`.


UPDATE: Actually, it turns out that `decodeTopLevelObject(of:forKey:)` is what I needed and it's still working well with `NSSecureCoding`, so I don't need to use delegate anymore. Thanks!