Bringing Polymorphism to Codable


Swift’s protocol oriented programming is just so helpful when dealing with polymorphic situations. When we need to persist this data we end up we some issues. Codable is not able to determine what concrete type to decode the saved data into when decoding. In this post, I will share a cut down version of the polymorphic Codable system that I have been using to build Cleora. I suggest you first take a look over my last post that introduced the use property wrappers in complex Codable situations.


Becoming Polymorphic


When encoding polymorphic types it is important to include an identifier of the concrete type that is being encoded, this will be used later in the decoding stage.


To enable this we can start out defining the new protocol that all our polymorphic types should conform to:


protocol Polymorphic: Codable {
  static var id: String { get }
}


For convince we can also define a default implementation on this using this extension:

extension Polymorphic {
  static var id: String {
    String(describing: Self.self)
  }
}


We need to use this when encoding a value. So we define a method on the Encoder that will write out any value (if it is polymorphic) and adds the concrete type id to the encoded JSON.


extension Encoder {
  func encode<ValueType>(_ value: ValueType) throws {
    guard let value = value as? Polymorphic else {
      throw PolymorphicCodableError.unableToRepresentAsPolymorphicForEncoding
    }
    var container = self.container(
      keyedBy: PolymorphicMetaContainerKeys.self
    )
    try container.encode(type(of: value).id, forKey: ._type)
    try value.encode(to: self)
  }
}


We should not attempt to constrain ValueType to conform to Polymorphic as in many situations at compile time ValueType will be a protocol and unfortunately Swift does not let us constrain the inheritance of protocols.


In the above encoding method, the type is encoded onto the _type field alongside the fields from the struct. You might find that in your use case it is better to encode it into a different field name or even encode the data of the struct into a nested field. This will likely depend on if you need to read/write this data server-side, where you might already have a polymorphic coding style provided by your server framework.


In addition to the above method we need to define some errors and other structs used in the encoding and decoding methods:


enum PolymorphicCodableError: Error {
  case missingPolymorphicTypes
  case unableToFindPolymorphicType(String)
  case unableToCast(decoded: Polymorphic, into: String)
  case unableToRepresentAsPolymorphicForEncoding
}

enum PolymorphicMetaContainerKeys: CodingKey {
  case _type
}


Decoding


This is quite a bit more complex. Firstly we need to read out the _type value and look for a matching concrete type to decode into. Then we can attempt to decode into that type, and finally since the caller of this method does not know the concrete type that will be returned will need to cast to that protocol and return.


extension Decoder {
  func decode<ExpectedType>(_ expectedType: ExpectedType.Type) throws -> ExpectedType {
    let container = try self.container(keyedBy: PolymorphicMetaContainerKeys.self)
    let typeID = try container.decode(String.self, forKey: ._type)
     
    guard let types = self.userInfo[.polymorphicTypes] as? [Polymorphic.Type] else {
      throw PolymorphicCodableError.missingPolymorphicTypes
    }
     
    let matchingType = types.first { type in
      type.id == typeID
    }
     
    guard let matchingType = matchingType else {
      throw PolymorphicCodableError.unableToFindPolymorphicType(typeID)
    }
     
    let decoded = try matchingType.init(from: self)
     
    guard let decoded = decoded as? ExpectedType else {
      throw PolymorphicCodableError.unableToCast(
        decoded: decoded,
        into: String(describing: ExpectedType.self)
      )
    }
    return decoded
  }
} 

As above we should not constrain ExpectedType to conform to Polymorphic as in many situations at compile time ExpectedType will be a protocol and unfortunately Swift does not let us constrain the inheritance of protocols.


The list of possible types can be persisted in the userInfo of the Decoder. For this, we need to create a new CodingUserInfoKey.


extension CodingUserInfoKey {
  static var polymorphicTypes: CodingUserInfoKey {
    CodingUserInfoKey(rawValue: "com.codable.polymophicTypes")!
  }
}


Then when creating our decoder we add our polymorphic Types.


var decoder = JSONDecoder()
decoder.userInfo[.polymorphicTypes] = [
  Snake.self,
  Dog.self
]


Using this without lots of boilerplate


Now that we have encode and decode methods that handle these Polymorphic types we need to use them within our app. We could (and sometimes might) use them directly when writing custom encode(to:) and init(from:) methods. But we can also define a Property wrapper that helps us avoid needing to write these custom methods for our structs.


@propertyWrapper
struct PolymorphicValue<Value> {
  var wrappedValue: Value
}

extension PolymorphicValue: Codable {
  init(from decoder: Decoder) throws {
    self.wrappedValue = try decoder.decode(Value.self)
  }
   
  func encode(to encoder: Encoder) throws {
    try encoder.encode(self.wrappedValue)
  }
}

We should not constrain Value to conform to Polymorphic as at compile time Value will always be a protocol and unfortunately Swift does not let us constrain the inheritance of protocols.


To use this property wrapper within our structs is easy:

struct UserRecord: Codable {
  let name: String
  let dateOfBirth: Date
   
  @PolymorphicValue
  var pet: Animal
}


Swift is now able to automatically provide Codable conformance avoiding the need to write any custom encode/decode methods.