decodeBool cannot fail, but may crash

I don't know how to handle this situation properly.


I have coded a value in a class.

When I decode, it should be a Bool, but it may be an Object (if created by an older version of the application, in Swift2 times).


So I used to do this :

     var test : Bool
        if decoder.containsValue(forKey: detailOptionsKey) {      // key exists ; but was it created as Bool or as object (in Swift2)
           test = decoder.decodeObject(forKey: detailOptionsKey) as? Bool ?? decoder.decodeBool(forKey: detailOptionsKey)
        } else {
            test = false
        }

But In some cases I get the following crash message :


[General] *** -[NSKeyedUnarchiver decodeBoolForKey:]: value for key (detailOptionsKey) is not a boolean


My problem is that in :

test = decoder.decodeObject(forKey: detailOptionsKey) as? Bool ?? decoder.decodeBool(forKey: detailOptionsKey)

I have no way to test if decodeBool can be called ; but I need to call if value was encoded as a Bool (not an object)


I do not see how to test to avoid a crash.

I wonder if I could use decodeDecodable which can fail (that's what I need)

func decodeDecodable<T>(_ type: T.Type, forKey key: String) -> T? where T : Decodable


I tried

            let test = (decoder as! NSKeyedUnarchiver).decodeDecodable(Bool.self, forKey: detailOptionsKey) ?? false
            self.detailOptions = decoder.decodeObject(forKey: detailOptionsKey) as? Bool ?? test

which seems to work, but a bit convoluted.

Why do I need Bool.self ? and not just Bool


In anycases, as decodeBool (or decodeInteger, …) may crash, I would need to make my code more robust.

How can we know the type of the key, to use the appropriate decoder ?

Accepted Reply

>> decodeDecodable


No, you can't do that. "Decodable" (part of "Codable") is the new Swift-only protocol that implements a different mechanism to "NSCoding". You can embed Codable things in a NSCoding archive, but they're not interchangeable with things encoded the old way.


>>Really surprising that the new decode (ie decodeBool) cannot fail but just crash. That seems a very strange design decision not to return an optional or throw an exception. Cannot understand why, when decodeObject returns optional.


You're dealing with a (very old) Obj-C API here. "decodeObject" can return nil because "encodeObject" can put nil in an archive. The nil result is not an error return.


Traditionally, NSCoding's decoding has no error returns. That's because the decoding is done in an "init" method that has no way of indicating an error. (It can return nil, but — as in the "decodeObject" case — nil can sometimes be a valid value.) However, a few years ago, an error return mechanism was added to NSKeyedUnarchiver, but it's confusing because the error is hidden until decoding returns to the top level. To use this mechanism, you need to start decoding using one of the "decodeTopLevel…" methods. As Quinn said, to catch the "bool" decoding failure, you will also need to set the decoding policy to not throw exceptions.


Having said all this, the real problem is that you painted yourself into a corner. You should never have re-used the same key for a different kind of value. The usual way of handling this is to use a different key for the new value type, and to try decoding both keys to see what's there. It's also usual to encode some kind of archive version into the archive itself, which can serve as a discriminator when you accidentally create a problem like this.


You might be able to solve your problem by trying two different decodes at the top level, starting from "decodeTopLevel…", and setting a global variable (or something) to indicate which decode to apply to the bool-or-not key, and setting the appropriate decode policy. If it's not a bool, the entire decode will fail when you get back to the top level, and then you can try the other one.


If that turns out to be impossible, your best strategy might be to write a compatibility method in Obj-C, which wraps the decoding in an @try/@catch construct that should catch the Obj-C exception, and call that method from Swift.

Replies

What’s happening here is that the keyed unarchiver’s

NSCoder
implementation is throwing an Objective-C language exception when it detects the type mismatch, which is the traditional behaviour for this sort of thing. You can disable this by setting the coder’s
decodingFailurePolicy
property to
.setErrorAndReturn
. However, there are some serious caveats here. You should read the doc comments in
<Foundation/NSCoder.h>
for the details.

If I were in your shoes I’d just rewrite this code using Swift 4’s serialisation support; it’s much nicer.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for the reply.


The problem was that it may happen (that's how I detected the problem), that data were encoded with an older version, which for whatever reason fails.


For robustness of the new versions, I want to handle those case gracefully: cannot decode, so reset to a default value. But not crash !

So I want to detect the type mismatch and act on it.

But this does not work:

            let test = (decoder as! NSKeyedUnarchiver).decodeDecodable(Bool.self, forKey: detailOptionsKey) ?? false
            self.detailOptions = decoder.decodeObject(forKey: detailOptionsKey) as? Bool ?? test


Because decodeDecodable(Bool.self, forKey: detailOptionsKey) always fais and return nill.

I thought it would give the same result as decodeBool() when the type is correct, but does not seem to be the case.

Sorry for insisting, but would like to know if and how I can use decodeDecodable ; what type argument to pass ?


When you propose Swift 4’s serialisation support, what's more than decodeBool for Bool ?

And how to test wether decodeBool will crash without calling (I hoped decodeDecodable(Bool 🙂 would do this.


I tried to set decodingFailurePolicy.

I did this at beginning of init((coder decoder: NSCoder)

But I get a crash: unarchiver has started ; cannot change decoding error policy.

Where should I do this ?


SO AS A SUMMARY:

how can I detect decodeBool cannot decode a key, without calling decodeBool and crashing ?

About the only thing I can suggest is cast as an NSNumber and then see if the contained value is 0 or non-zero. Hopefully that will work.


Beyond that, I don't have a solution that would allow you to keep using the NSCoder, etc. APIs.


Back in Swift 1.0, many including myself struggled with numerous bugs. In my case, it stalled my porting to Swift for months, so ended up writing my own APIs. Timings are very good; beats NSCoding for read and write (albeit order does matter in my case whereas keyed-archiving can be read/written in any order). I also got complete safety. Files are read into a work buffer and all read APIs are protected to not read beyond it. Default values provided to the APIs will be used if read-errors occur or if values are outside specified ranges.

I tried this, testing:

if let value = decode.decodeObject(forKey: "key") as? NSNumber


But that always fail (returns nil)


When I look at the file (in textEdit), I see the key followed by ] or ^(asci 93 or 94) ; seems similar in a working file and a corrupted file. Don't know if it's the usual coding of Bool

Even if quasi impossible to decode:

Corrupted file: _ detailOptionsKey] Means 0 (false)?

&01*3&56&89:;<__ dynamicLinksKey_ lastModifListesDateKeyZuseLogoKey_ groupesNamesKey_ verifDataParamKey_ edTEnHauteurKey_ detailOptionsKey]whenToMoveKey^

valid file: same _ detailOptionsKey]

%&'('*((-..'12('5(78(:;<=> dynamicLinksKey_ lastModifListesDateKeyZuseLogoKey_ groupesNamesKey_ verifDataParamKey_ edTEnHauteurKey_ detailOptionsKey]whenToMoveKey^


Really surprising that the new decode (ie decodeBool) cannot fail but just crash. That seems a very strange design decision not to return an optional or throw an exception. Cannot understand why, when decodeObject returns optional.


I must miss something.

>> decodeDecodable


No, you can't do that. "Decodable" (part of "Codable") is the new Swift-only protocol that implements a different mechanism to "NSCoding". You can embed Codable things in a NSCoding archive, but they're not interchangeable with things encoded the old way.


>>Really surprising that the new decode (ie decodeBool) cannot fail but just crash. That seems a very strange design decision not to return an optional or throw an exception. Cannot understand why, when decodeObject returns optional.


You're dealing with a (very old) Obj-C API here. "decodeObject" can return nil because "encodeObject" can put nil in an archive. The nil result is not an error return.


Traditionally, NSCoding's decoding has no error returns. That's because the decoding is done in an "init" method that has no way of indicating an error. (It can return nil, but — as in the "decodeObject" case — nil can sometimes be a valid value.) However, a few years ago, an error return mechanism was added to NSKeyedUnarchiver, but it's confusing because the error is hidden until decoding returns to the top level. To use this mechanism, you need to start decoding using one of the "decodeTopLevel…" methods. As Quinn said, to catch the "bool" decoding failure, you will also need to set the decoding policy to not throw exceptions.


Having said all this, the real problem is that you painted yourself into a corner. You should never have re-used the same key for a different kind of value. The usual way of handling this is to use a different key for the new value type, and to try decoding both keys to see what's there. It's also usual to encode some kind of archive version into the archive itself, which can serve as a discriminator when you accidentally create a problem like this.


You might be able to solve your problem by trying two different decodes at the top level, starting from "decodeTopLevel…", and setting a global variable (or something) to indicate which decode to apply to the bool-or-not key, and setting the appropriate decode policy. If it's not a bool, the entire decode will fail when you get back to the top level, and then you can try the other one.


If that turns out to be impossible, your best strategy might be to write a compatibility method in Obj-C, which wraps the decoding in an @try/@catch construct that should catch the Obj-C exception, and call that method from Swift.

Thanks for the detailed explanation.


I know I've created the problem, and I can probably get out of it.


But, in a more general approach, it could happen (whatever reason) that encoded data is corrupted and cannot be decoded. Having a way to gracefully fail, give it a default value and encode back to a correct coding would be great.


And, AFAIK, that was the case with decodeObject !

Again, there is a mechanism to make it fail gracefully ("decodeTopLevel…" and "failWithError:") but it takes some effort and care to implement.


>> And, AFAIK, that was the case with decodeObject


It was partially the case with decodeObject. For a custom object, where you provide the "init?(coder:)", if something goes wrong you can return nil to indicate the error. But there is a hierarchy, and the next level must propagate the nil value upwards. That makes nil a "global" error indicator for your decoding process, which works until you genuinely have optional values that can return nil. When those two heuristics collide, there's no way to resolve what's going on.


This is also messier in Swift than it would be in Obj-C, since Swift actually wraps the Obj-C methods in its own compatibility layer.


Quinn>>If I were in your shoes I’d just rewrite this code using Swift 4’s serialisation support; it’s much nicer.


This is very good advice.

2 of you with the same strong advice, I should foillow.


I've looked at seroialization, but I do not see what to change. As values should be simple Bool, what should I do to rewrite this code using Swift 4’s serialisation support ?


Thanks for your patience.

There are two parts to this.


1. You should adopt the Codable protocol for your types that will participate in archiving. This consists of the Encodable and Decodable subprotocols. In many cases, adopting the protocols is all you need to do — there's no code to be written. If you have to write any code, it's pretty simple.


Note that when using Codable for archiving purposes, you still have to think clearly about how your archive can evolve over time. This affects what keys you use, and what types of values are associated with each key.


2. You still need to use NSKeyedArchiver and NSKeyedUnarchiver to create the archive data. Use "encodeEncodable" with the top level object to start encoding, and use "decodeTopLevelDecodable" to start decoding. That should give you "normal" error handling throughout the process. (Note that you can't use the static convenience methods. You have to create the archiver/unarchiver object explicitly, then invoke the top level encoding/decoding.)