
This video is only available to subscribers. Start a subscription today to get access to this and 469 other videos.
Decoding Heterogeneous Arrays
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.