Should I stick with Codable or switch back to NSCoding?

Hi,
I recently rewrote my whole project to be based on Codable instead of NSCoding. But I ran into multiple problems, which make me doubt that the switch was the right idea. So I am asking your opinion about this and whether you think I should stick with Codable and try to find workarounds/solutions to the newly arisen problems or go back to NSCodable where those problems did not exist for me.

Here is why is switched to Codable (pros) and why it might be a good idea to go back NSCoding (cons):

pros of Codable so far for me:

  • getting rid of objc dependency seems like a good idea to me, I do not need NSObject dependency any more for most of my classes
  • I thought Codable would be the future and would succeed NSCoding in the near future, but I see now that I might have been wrong about that
  • it might make sense for me to use async/await in conjunction with Codable, which does not seem to be possible with NSCoding (as far as I know init?(coder: NSCoder) cannot be defined as async)

cons so far:

  • Codable does not support Polymorphism (implemented my own workaround for this), which is a must have for me
  • Codable does not support decoding of multiple references to the same instance (or is there any built-in option I did not see so far?), which is also a must have for me
  • encoding/decoding generic types seems to be a pain and I could use that functionality, too

I recently read the following note by @jrose:

While my project data structure is mainly tree based, I also use graph structures which leads to the problem of having multiple references to the same object and having trouble to decode them properly.

So to sum things up:
I am having plenty of trouble replacing NSCoding with Codable in my project and it feels I am doing a lot of extra lifting and ending up with a more bloated and error-prone code (for supporting polymorphism, multiple references and generics) than I would have with NSCoding. I like the move away from Objective-C and would like to stick with Codable especially after all the work I already put into it, but maybe it is the wise choice to cut my losses and switch back to NSCoding? I am especially interested in what you think about the cons above and if I am missing obvious solutions.

Well, FWIW, these are the main benefits to using NSCoding over Codable. See a tangentially-related post here, but these requirements would be the main reason I would recommend someone sticking with NSCoding over switching to Codable.

As you note, Codable doesn't support polymorphism in the same way that NSCoding does (and this is a feature), but at least it can be worked around depending on your specific requirements. Circular object graphs are only partially supported (well, this depends on the Encoder/Decoder and none that I'm aware of readily support it), and are a much bigger pain to work around.

Based on this alone, it sounds to me like your requirements make NSCoding the right tool for the job. As @jrose notes in that quoted comment, NSCoding isn't going away, and is fully-supported tooling; if it's the tool that best solves your problem, use it!

At least on this point, you can keep your types as migrated to Swift, but adopt NSCoding on them instead.


it might make sense for me to use async/await in conjunction with Codable,

As an aside: Codable as written doesn't support async/await in any meaningful way, and I'm not sure that it could. If we wanted async initialization and encoding, I think the Codable system would likely need to be rewritten from the ground up.

1 Like

Thank you very much for your insights. As far as I can tell you are one of the heroes helping lots of people on these forums. I do appreciate this very much!

One tiny question regarding your comment:

At least on this point, you can keep your types as migrated to Swift, but adopt NSCoding on them instead.

There is no sensible way of getting rid of NSObject inheritance when using NSCoding, right? You mean inherit from NSObject, implement necessary NSCoding functionality and keep the rest as swifty as possible, right?

1 Like

Indeed! That would be the easiest way to go about it.

Mild Pedantry

NSKeyedArchiver and NSKeyedUnarchiver expect the values passed to them behave like Obj-C objects insofar as they expect to be able to call memory management methods on them, as well as a few methods like +classForCoder and others. Technically, you can avoid inheriting from NSObject by implementing all of these methods directly on your type... But there's no real benefit to it, and it'd be easy to miss a method. Inheriting from NSObject gives you default implementations for all necessary methods, present and future, so you may as well go with that.

2 Likes

Hi,

This is my first time posting on this forum so please excuse any faux-pas :) I've come across this thread after asking a question on both hackingwithswift and stackoverflow and getting no response. The question I've asked is essentially "How the hell do I encode/decode inherited classes, and get the subclasses back?" Here's a link to my original post

[Loading JSON ignores different subclasses – Swift – Hacking with Swift forums]

From the above discussion it seems I'm not alone in having this problem and that NSCoding is the way to go - is that correct? If so, would you be able to point me in the direction of any resources that might assist me?

Thanks in advance,

Jes

Hi Jes,
you have two options here: Either you use NSCoding, which supports Polymorphism (i.e. encoding and decoding of subclasses using type information of superclasses) out of the box or you have to extend Codable yourself. While not trivial, you can extend Codable to do this, but Codable also does not support encoding decoding of multiple references to the same instance, no cyclic references and so on. I found myself in the situation that I needed Polymorphism support and multiple reference encoding, so I switched back to using NSCoding, which supports those things out of the box.

Here are my hints for either way:

Using NSCoding:
You will find plenty information on that online, essentially you subclass you custom class from NSObject and make it conform to NSCoding by implementing encode(with:) and init?(coder:). If you are familiar with custom Codable implementations, this should be very familiar to you.

Using Codable:
This gets a little bit tricky and here is my code for supporting Polymorphism (no support for multiple reference encoding). What it does is it essentially uses an enum to encode type information (which Codable does not do on its own) with your object when encoding and uses this type information to correctly decode the subclass in the decoding step. This involves manually adding all your custom types (which you want to support Polymorphism) to the PolymorphicType enum (replace BaseClass, ChildClass and ChildChildClass below with your types). Also any of your custom types, which should support Polymorphic encoding/decoding need to adopt the Polymorphic protocol. If that has been done the code below has all the necessary boiler-plate to do all the rest of the work for you. The only thing you have to do is use polymorphicEncode and polymorphicDecode instead of enocode and deocde. Those functions are supported for PropertyListEncoder and any KeyedEncodingContainer, but you can just use the same extension in the code if you want to extend JSONEncoder. Same for decoding. There is essentially quite some boilerplate to support Arrays of your custom types and also Optionals, too, so you could encode [CustomType].self and [CustomType]?.self and so forth. If you have any questions regarding this code let me know. I might upload it to GitHub one day with example code, but have not done so yet.

enum PolymorphicType: String, CodingKey {
    case base
    case child
    case childChild
    
    var type: any Polymorphic.Type {
        switch self {
        case .base:
            return BaseClass.self
        case .child:
            return ChildClass.self
        case .childChild:
            return ChildChildClass.self
        }
    }
}

/// objects complying to this protocol are Codable and can be used with polymorphic Codable extension
protocol Polymorphic: Codable {
    associatedtype Converted
    
    static var wrappedType: Self.Converted.Type { get }
//    static var optionalWrappedType: Self.Converted?.Type { get } // FIXME: testing -> remove
    static var polymorphicType: PolymorphicType { get }
}


extension Polymorphic {
    typealias Converted = PolymorphicWrapper
    
    static var wrappedType: Self.Converted.Type {
        return Self.Converted.self
    }
}


protocol OptionalProtocol {
    var isSome: Bool { get }
    func unwrap() -> Any?
}

extension Optional: Polymorphic where Wrapped: Polymorphic {
    typealias Converted = Wrapped.Converted?
    
    static var polymorphicType: PolymorphicType {
        return .base
    }
}


extension Optional: OptionalProtocol {
    var isSome: Bool {
        switch self {
        case .none:
            return false
        case .some:
            return true
        }
    }
    
    func unwrap() -> Any? {
        switch self {
        case .none:
            return nil
        case .some(let unwrapped):
            return unwrapped
        }
    }
}


// array extension to make nested arrays of inner type Polymorphic also Polymorphic; also enforces static var polymorphicType for encoding purposes as Metatypes cannot be encoded by Codable
extension Array: Polymorphic where Element: Polymorphic {
    typealias Converted = [Element.Converted]
    
    static var polymorphicType: PolymorphicType {
        return .base
    }
}


struct PolymorphicWrapper: Codable {
    private enum CodingKeys: String, CodingKey {
        case value
        case type
    }
    
    var reference: any Polymorphic
    
    // MARK: initialization
    
    init(_ reference: any Polymorphic) {
        self.reference = reference
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        let polymorphicType = PolymorphicType(rawValue: try container.decode(String.self, forKey: .type))!
        
        self.reference = try container.decode(polymorphicType.type, forKey: .value)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(self.reference, forKey: .value)
        try container.encode(type(of: self.reference).polymorphicType.rawValue, forKey: .type)
    }
    
    // MARK: static interface
    
    static func recursivePolymorphicWrap<T: Polymorphic>(_ input: T) -> T.Converted {
        if let array = input as? [any Polymorphic] {
            return array.map { recursivePolymorphicWrap($0) } as! T.Converted
        } else if let optional = input as? OptionalProtocol {
            if let unwrapped = optional.unwrap() as? any Polymorphic {
                return Optional(recursivePolymorphicWrap(unwrapped)) as! T.Converted
            } else {
                return Optional<any Polymorphic>.none as! T.Converted
            }
        } else {
            return PolymorphicWrapper(input) as! T.Converted
        }
    }
    
    static func recursivePolymorphicUnwrap(_ input: Any) -> Any {
        if let array = input as? [Any] {
            return array.map { recursivePolymorphicUnwrap($0) }
        } else if let wrapper = input as? PolymorphicWrapper {
            return wrapper.reference
        } else {
            return input
        }
    }
}


extension PropertyListEncoder {
    func encodePolymorphic(_ value: any Polymorphic) throws -> Data {
        let wrappedValue = PolymorphicWrapper.recursivePolymorphicWrap(value) as! Encodable
        return try self.encode(wrappedValue)
    }
}


extension PropertyListDecoder {
    func decodePolymorphic<T: Polymorphic>(_ inputType: T.Type, from data: Data, format: inout PropertyListSerialization.PropertyListFormat) throws -> T {
        let transformedType = inputType.wrappedType as! Decodable.Type
        let wrappedValue = try self.decode(transformedType, from: data, format: &format)
        return PolymorphicWrapper.recursivePolymorphicUnwrap(wrappedValue) as! T
    }
}


extension KeyedEncodingContainer {
    mutating func encodePolymorphic(_ value: any Polymorphic, forKey key: KeyedEncodingContainer.Key) throws {
        let wrappedValue = PolymorphicWrapper.recursivePolymorphicWrap(value) as! Encodable
        try self.encode(wrappedValue, forKey: key)
    }
}

extension KeyedDecodingContainer {
    func decodePolymorphic<T: Polymorphic>(_ inputType: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T {
        let transformedType = inputType.wrappedType as! Decodable.Type
        let wrappedValue = try self.decode(transformedType, forKey: key)
        return PolymorphicWrapper.recursivePolymorphicUnwrap(wrappedValue) as! T
    }
}

Hi Benjamin,

Thank you so much for responding, and in so much detail too - I really appreciate you getting back to me. I'll take a good look over what you've suggested and see where that takes me.

Thanks again,

Jes