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.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Level up your Swift skills!$35

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms

Level up your Swift skills!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35

# 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
}
Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Enhance older apps with SwiftUI!$45

A detailed guide on gradually adopting SwiftUI in UIKit projects

Updated for iOS 18 and Xcode 16!

Integrating SwiftUI into UIKit Appsby Natalia Panferova

  • Upgrade your apps with new features like Swift Charts and Widgets
  • Support older iOS versions with effective backward-compatible strategies
  • Seamlessly bridge state and data between UIKit and SwiftUI using the latest APIs

Enhance older apps with SwiftUI!

A detailed guide on gradually adopting SwiftUI in UIKit projects

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

Integrating SwiftUI
into UIKit Apps

by Natalia Panferova

Updated for iOS 18 and Xcode 16!

$45

# 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
}
Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Level up your Swift skills!$35

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms

Level up your Swift skills!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35