Episode #395

Decoding Heterogeneous Arrays

22 minutes
Published on June 27, 2019

This video is only available to subscribers. Get access to this video and 584 others.

You may encounter a scenario where you want to decode some JSON that contains an array of objects that may be of a different type. In this episode we examine such a scenario, where we have a feed that contains an array of posts, but each post object can be of a different kind, such as text, image, or video. We will take a look at how to solve this by introducing a protocol called DecodableClassFamily, and along with a Discriminator that will inform the decoding logic which type it should decode. We'll then take this working example and make a reusable solution using Swift Generics.

Episode Links

Implement Feed To Obtain Post

Imagine we have a Feed that has an array of Posts.


class Feed : Codable {
    var posts: [Post] = []

    init() {
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.posts = try container.decodeHeterogeneousArray(family: PostClassFamily.self, forKey: .posts)
    }
}

Each post can be of a different type (text, image, video, etc) and there's nothing here that informs the Decoder what type to decode.

Create Decoder For Nested Unkeyed Container

Here we will implement decoding in multiple steps:
1) First, to get the key from an array, we will get the container using .nestedUnKeyedContainer for the key Post.
2) We will then start decoding the post container using .nestedContainer for the type described in the Discriminator.
3) We have to create a copy of the container to allow us to read the same content more than once. Since reading keys from an unkeyed container advances to the next element, there's no way to go back and decode it again with a different type. So we have to use 2 containers, one for peeking, and one for decoding the type we want. The decoded posts will then be appended.
4) To reuse the decoder, we will create an extension KeyedDecodingContainer for heterogeneous collections for the family of DecodableClassFamily with its base type and ensure that the decodable type is from DecodableClassFamily.
5) We will also ensure that our code handles decoding error while decoding.


extension KeyedDecodingContainer {
    func decodeHeterogeneousArray<F : DecodableClassFamily>(family: F.Type, forKey key: K) throws -> [F.BaseType] {

        var container = try nestedUnkeyedContainer(forKey: key)
        var containerCopy = container
        var items: [F.BaseType] = []
        while !container.isAtEnd {

            let typeContainer = try container.nestedContainer(keyedBy: Discriminator.self)
            do {
                let family = try typeContainer.decode(F.self, forKey: F.discriminator)
                let type = family.getType()
                // decode type
                let item = try containerCopy.decode(type)
                items.append(item)
            } catch let e as DecodingError {
                switch e {
                case .dataCorrupted(let context):
                    if context.codingPath.last?.stringValue == F.discriminator.stringValue {
                        print("WARNING: Unhandled key: \(context.debugDescription)")
                        _ = try containerCopy.decode(F.BaseType.self)
                    } else {
                        throw e
                    }
                default: throw e
                }
            }
        }
        return items
    }
}

Decodable Class Family

We will now, create a protocol DecodableClassFamily of type decodable, with a discriminator to decode the type of the object and a function getType. To ensure that known type is decoded, we will create a BaseType.


enum Discriminator : String, CodingKey {
    case type
}

enum PostClassFamily : String, DecodableClassFamily {

    typealias BaseType = Post

    case text
    case image

    static var discriminator: Discriminator { return .type }

    func getType() -> Post.Type {
        switch self {
        case .text: return TextPost.self
        case .image: return ImagePost.selft  
        }
    }
}

Handling Error For Unknown Types

To handle the error for unknown type, we can follow one of the below ways:

1) We can ensure that our DecodableClassFamily has its decodable error implemented by adding key unknown in its concrete class. We can then have an optional getType or can have a condition, set for unknown types.

2) Alternatively, we can handle this error in our generic function.

This episode uses Xcode 10.2.1, Swift 5.0.