Encode and decode polymorphic types in Swift

Swift’s protocol oriented programming is very helpful when dealing with polymorphic situations. But when we need to persist polymorphic data, we encounter some issues: Codable is not able to determine what concrete type to decode the saved data into.

In this post, I will share a cut down version of the polymorphic Codable system that I have been using in my apps. I suggest you first take a look at my last post Reduce Codable Boilerplate with the Help of Property Wrappers that introduced how to use property wrappers in complex Codable situations.

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Check out our book!

Integrating SwiftUI into UIKit Apps

Integrating SwiftUI intoUIKit Apps

UPDATED FOR iOS 17!

A detailed guide on gradually adopting SwiftUI in UIKit projects.

  • Discover various ways to add SwiftUI views to existing UIKit projects
  • Use Xcode previews when designing and building UI
  • Update your UIKit apps with new features such as Swift Charts and Lock Screen widgets
  • Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit

# Polymorphic protocol

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

We can start by defining a new protocol that all our polymorphic types should conform to.

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

For convenience we can also define a default implementation using an extension.

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

# Encoding

We use our Polymorphic protocol when encoding a value. We define a method on the Encoder that writes out any value conforming to 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. 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 encoding method the type is encoded into the _type field alongside other 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 whether you need to read and write this data on the server, where you might already have a polymorphic coding style provided by your server framework.

In addition, 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

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

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
    }
}

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

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

When creating the decoder, we add our polymorphic types.

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

# Usage

Now that we have encoding and decoding methods that handle polymorphic types, we can use them within our app. We could use them directly when writing custom encode(to:) and init(from:) methods.

We can also define a property wrapper that helps us to avoid writing 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)
    }
}

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

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