Codable != Archivable

These are all great questions! To briefly answer the questions before diving into the details:

  • What you're looking to do, for various reasons detailed below, is best described and achieved via NSCoding
  • This is not currently possible to do in Swift, but I don't think we would want to make implicitly possible to encode objects in this way, for the same reasons that it was not done for Objective-C, where it is possible via the runtime
  • In this application, Python specifically benefits from being a dynamically-typed language, and pickle benefits from working under largely the same constraints that NSCoding does

To add some detail, in somewhat reverse order:

  • Python's pickle benefits from several things:
    1. Save for primitive types, almost everything in Python is an object. You cannot define structs in the same way that you can in Swift, and all objects are present in the runtime
    2. Python objects are easily introspectable at runtime, and Python has no real access restrictions like Swift does: nothing really prevents you from mucking about with objects without their knowledge. Importantly, this allows you to construct objects from a list of properties without necessarily needing to call their constructors
    3. Because Python doesn't have the same kind of access restrictions that Swift does, its class naming scheme is relatively simple, and not subject to significantly surprising behavior (detailed here, but I'll comment further on this)
  • As opposed to this, Swift has the following to consider:
    1. Swift is a strongly-typed, compiled language. This means that there are strong restrictions on how you can treat objects, and any archived format would need to take care to consider the types of objects being assigned to properties on deserialization. If an archive contains a dictionary where I expect there to be an integer, Swift cannot simply assign the dictionary to an integer property — the size, layout, and semantics of the types are completely different.

      Python, being so dynamic, has no problem assigning a dictionary where an integer would normally go, so unpickling is relatively straightforward, and this problem can be largely glossed over.

    2. Swift has access restrictions where Python is much more free-spirited about things. While I believe the runtime has the power to ignore some of these access restrictions in assigning properties at runtime, this is not exposed to users because it goes against the spirit of the language: Swift is simply stricter about these things, and has many rules in place to maintain order. Unlike Python objects, Swift objects are not typically "bags of stuff" which you can instantiate and place things into — to construct a Swift object, you must call an initializer and treat the object much more carefully to maintain type safety

    3. As part of these access restrictions, Swift allows whole types to be private or fileprivate, which has a marked effect on the actual class name — as detailed in the other post, moving a private or fileprivate class around can change its actual class name! pickle also glosses over this issue — if you rename a class or a module, there isn't much support without subclassing pickle.Unpickler and manually renaming the type yourself. If this is a private type owned by another library you don't know about, there isn't much you can do

    4. Swift, of course, also has structs to deal with, which have their own potential host of issues. Because structs are not objects, they don't participate in the runtime in the same way. I believe that private and fileprivate structs don't necessarily even have to have their names embedded in binaries or in the runtime depending on how they are used, making them inaccessible. It would be a real shame if this implicit encodability were possible for only classes, since many classes end up containing structs in properties, and so those classes could not participate (unfortunately, this is one of the drawbacks that NSCoding has as well, and requires manually encoding struct properties via various other strategies)

  • Objective-C is much closer to Python in its runtime capabilities, and could support arbitrary encoding of all objects, but doesn't do this in favor of formalizing the concept with NSCoding. There is a whole host of things that various objects prefer to do that are best represented by an opt-in API. For instance, weak properties, as you mention, usually require special attention — most objects don't need to encode or decode their delegates if they have them (since delegates tend to be runtime-state only, and don't even necessarily represent an encodable object)

So, to sum up: doing this implicitly would both go against the spirit of Swift, and isn't necessarily technically feasible without changes to the language. NSCoding is much more tailored to what you're trying to do, since its goals are exactly the use-case you describe. It does have the drawback of requiring NSObject inheritance in practice, but given the discussion in this thread and others, we are also interested in making object graphs easier to represent with Codable as well.


On one more note: pickle (and marshal) also doesn't really deal with security in any meaningful way:

Warning: The pickle module is not secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.

This is another drawback to doing something like this totally implicitly: you have no way of knowing what the object graph you meant to encode or decode might look like, so any pickled object graph can contain anything at decode time, which is Bad™ if they are maliciously altered. This is the reason for NSSecureCoding (to help describe what you expect your object graph to look like so you can prevent arbitrary code execution at runtime), and one of the reasons Codable is not tailored toward including class information in archives.

8 Likes