Codable clarification question.

this happens to be in the context of UserDefaults. I'm trying to track down an issue where a particular class is turned into it's superclass, either when saving to UserDefaults, or loading from UserDefaults.


the classes in question (the super class and the class that gets 'permanently coerced') are for all purposes have the same footprint in storage. Same properties. Just different behavior after being loaded. What is saved to UserDefaults is a Data object constructed from an array.

the array is defined as an array of the superclass type:


var items : [BKLayerController]


the saving and the loading code:

open var items : [BKLayerController] {
        get{
            var theObjs : [BKLayerController] = []
            if let theData = UserDefaults.standard.data(forKey: BKToolboxDefaults) as? Data{
                do {
                    theObjs = try PropertyListDecoder().decode([BKLayerController].self, from: theData)
                } catch {
                    debugPrint(error)
                }
            }
            return theObjs
        }
        
        set{
            
            do {
                let theData = try PropertyListEncoder().encode(newValue)
                UserDefaults.standard.set(theData, forKey: BKToolboxDefaults)
            } catch {
                debugPrint(error)
            }
        }
    }


the Defaults registration code:


        let layer = BKLayerController()
        layer.name = "layer"
        let textLayer = BKTextLayerCon()
        textLayer.name = "textLayer"
        

        
        do {
            let theData = try PropertyListEncoder().encode([layer,textLayer])
            UserDefaults.standard.register(defaults: [BKToolboxDefaults : theData ])
        } catch {
            
        }


the class "BKTextLayerCon"is a "BKLayerController" class (it's super class) upon loading of the Defaults. It may happen during the saving process, I still haven't figured out how to test that yet.


my question, is that when you load objects with the propertyListDecoder (and with Codable Decoders in general) do you need to specify specific types? If so, what would that look like? like this?

if let theData = UserDefaults.standard.data(forKey: BKToolboxDefaults) as? Data{
                do {
                    theObjs = try PropertyListDecoder().decode([[BKLayerController].self, [BKTextLayerCon].self], from: theData)
                } catch {
                    debugPrint(error)
                }
            }

Replies

I'm not an expert in Secure Coding syntax.


So here is a recent example that works (needed some time for making it work):


@objc class MyFavoris : NSObject, NSSecureCoding {

    var myData: [[String: Any]]?
 
    static var supportsSecureCoding: Bool {
        return true
    }
 
    override init() {
        // usual stuff
   }
 
    required init(coder decoder: NSCoder) {
     
        myData = decoder.decodeObject(of: [NSArray.self, NSDictionary.self, MyFavoriss.self, NSDate.self], forKey: theKey) as? [[String: Any]] ?? []
    }

Hi Claude31,

I think we might have a misunderstanding, but what you're saying is, roghly translatable to Codable, and it's interesting.

there's a substantial difference between encoding/decoding top level objects, and making one's class Codable compliant.


whatever the decoder that is passed into an Init(coder decoder: ) method (or whatever the convention is for the Codable version is) it's not the publically accessible decoders/coders. It's somethig private, that is not documented. I've done a LOT of reading, of the same shallow pool of available documentation. And the PropertyListDecoder (one of 2 available concrete decoder classes... and the other is a JSON verson) does not have a method for accessing the container. it's a long winded story but basically this is the format of a Codable init method:


required public init(from decoder: Decoder) throws {
// first we get a container.
        let container = try decoder.container(keyedBy: CodingKeys.self)
// then we can address the container, and try to get each property with a Key.
        name = try container.decode(String.self, forKey: .name)
        hint = try container.decode(String.self, forKey: .hint)
        modes = try container.decode([BKLayConMode].self, forKey: .modes)
        baseMode = try container.decode(BKLayConMode.self, forKey: .baseMode)
        selectedMode = try container.decode(BKLayConMode.self, forKey: .selectedMode)
        subCons = try container.decode([BKLayerController].self, forKey: .subCons)
// finally: super.init()
        super.init()

    }


anyway. at the top level, where you save or load your document, from a Data object... there's no access to that container. You have to create a single structure, or object to encapsulate your data.


return try PropertyListEncoder().encode(docData)

that's literally the only thing you can do. beleive me I've tried.


so I just tried to do this:

theObjs = try PropertyListDecoder().decode([BKLayerController.self, BKTextLayerCon.self], from: theData)

and that does not compile.


so I think... we have an answer to my question. not a direct answer, but at least i have a potential test case, that could solve my issue. personally I think it's a bit insane.


but if I just create a structure:

struct DefaultSavingStruct : Codable {
     var theArray : [BKLayerController] = []
}

throw the array into that structure, save the result... then I will have a predictable, single Type to load from the Data. And therefore I "should" be able to do something like this:

thestruct = try PropertyListDecoder().decode(DefaultSavingStruct.self, from: theData)
theObjs = thestruct.theArray


I'll test it and see.

nope. that wasn't it. perhaps something I'm doing wrong in the init methods... which are not getting called.

I was able to nail down when the issue is happening.

the class has it's encode method called, upon saving.

it does not have it's init(from:) method called on loading.


so it seems that there's something going wrong with the Save, after it leaves my hands.


I'm beginning to suspect a bug, rather than operator, error now. But if it were a bug, nothing at all would work. Codable would be worthless.

I'll see if I can duplicate the issue in a playground.

here's the issue that you can put into a playground:


import Cocoa

// first we need a 2 class hierarchy.

class baseClass : NSObject , Codable {
    var theValue: Float = 0.345
    var theName : String = "aName"
    var theType : String = "baseClass"
    
    enum CodingKeys: String, CodingKey {
        case theValue
        case theName
    }
    
    override init() {
        
    }
    
    public init(name: String, val : Float){
        self.theName = name
        self.theValue = val
        super.init()
    }
        
    required public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        theName = try container.decode(String.self, forKey: .theName)
        theValue = try container.decode(Float.self, forKey: .theValue)
        
        super.init()
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(theName, forKey: .theName)
        try container.encode(theValue, forKey: .theValue)
    }
}



class subClass : baseClass{
    var isSub : Bool = true
    
    override init(name: String, val : Float){
        super.init(name: name, val: val)
        self.theType = "subClass"
    }
    
    required public init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        self.theType = "subClass"
    }
}

// so we start out with an array:
let myArray : [baseClass] = [subClass(name: "ob01", val: 1.0), subClass(name: "obj02", val: 2.2), subClass(name: "obj03", val: 3.3)]

// now let's turn it into a Data object. that should be enough.


var theData : Data? = nil
do {
    theData = try PropertyListEncoder().encode(myArray)
} catch {
    debugPrint(error)
}


// then we pull the objects out of the data.
var theObjs : [baseClass] = []
do {
    let theNewObjs = try PropertyListDecoder().decode([baseClass].self, from: theData!)
    theObjs += theNewObjs
} catch {
    debugPrint(error)
}

// now a simple test:
for each in theObjs{
    print(each.theType)
}



the goal, is to save an array that is a SuperClass of whatever is in the array, allowing different behavior for different objects.

and then when those objects are loaded again, they are of the same class type they were when they were saved, instead of the base class.


I don't think I'm doing anything wrong here, And there doesn't seem to be any opportunity to provide for options that allow this behavior.


and this is kind of funny: if you try it the other way... if you were to do this :

let theNewObjs = try PropertyListDecoder().decode([subClass].self, from: theData!)

it would coerce the classes to be the subclass type even if they were of the baseClass type.


I think this might be unintended consequenes of Apple trying to lock down security. I may very well be doing something wrong. But I am more and more suspicious that it's a bug.

do you need to specify specific types?

You need to specify a specific type.


If so, what would that look like? like this?

No way. You cannot decode non-uniform Array with Decodable.

OK... then how WOULD you decode a non-uniform Array?

You may need some heavy trick like this:


class BaseClass : Codable {
    var theValue: Float = 0.345
    var theName : String = "aName"
    
    private enum CodingKeys: String, CodingKey {
        case theValue
        case theName
    }
    
    public init(name: String, val : Float) {
        self.theName = name
        self.theValue = val
    }
}

class SubClass : BaseClass {
    var isSub : Bool = true
    
    public override init(name: String, val : Float) {
        super.init(name: name, val: val)
    }
    
    private enum CodingKeys: String, CodingKey {
        case isSub
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.isSub = try container.decode(Bool.self, forKey: .isSub)
        try super.init(from: decoder)
    }
    
    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(isSub, forKey: .isSub)
    }
}

enum TheType: Int, Codable {
    case baseClass
    case subClass
}

enum UnionClass: Codable {
    case baseClass(BaseClass)
    case subClass(SubClass)
    
    enum CodingKeys: CodingKey {
        case theType
        case baseClass
        case subClass
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let theType = try container.decode(TheType.self, forKey: .theType)
        switch theType {
        case .baseClass:
            self = .baseClass(try container.decode(BaseClass.self, forKey: .baseClass))
        case .subClass:
            self = .subClass(try container.decode(SubClass.self, forKey: .subClass))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .subClass(let sub):
            try container.encode(TheType.subClass, forKey: .theType)
            try container.encode(sub, forKey: .subClass)
        case .baseClass(let base):
            try container.encode(TheType.baseClass, forKey: .theType)
            try container.encode(base, forKey: .baseClass)
        }
    }
    
}

let myArray: [UnionClass] = [
    .subClass(SubClass(name: "ob01", val: 1.0)),
    .subClass(SubClass(name: "obj02", val: 2.2)),
    .subClass(SubClass(name: "obj03", val: 3.3))]

var theData : Data? = nil
do {
    theData = try PropertyListEncoder().encode(myArray)
} catch {
    debugPrint(error)
}


// then we pull the objects out of the data.
var theObjs : [UnionClass] = []
do {
    let theNewObjs = try PropertyListDecoder().decode([UnionClass].self, from: theData!)
    theObjs += theNewObjs
} catch {
    debugPrint(error)
}

// now a simple test:
for each in theObjs {
    print(each)
}


You have another option: do not use Codable.