Skip to content

Instantly share code, notes, and snippets.

@Kdan
Last active February 13, 2022 21:27
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kdan/270e1ea776c3dd056474261b523b0a56 to your computer and use it in GitHub Desktop.
Save Kdan/270e1ea776c3dd056474261b523b0a56 to your computer and use it in GitHub Desktop.
A Playground with an example of decoding a heterogeneous collection directly as a return type.
import Foundation
// MARK: - Model classes
/// The Pet superclass.
class Pet: Codable {
/// The name of the pet.
let name: String
enum CodingKeys: String, CodingKey {
case name
}
}
class Cat: Pet {
/// A cat can have a maximum of 9 lives.
var lives: Int
enum CatCodingKeys: String, CodingKey {
case lives
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CatCodingKeys.self)
lives = try container.decode(Int.self, forKey: .lives)
try super.init(from: decoder)
}
}
class Dog: Pet {
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
func fetch() { /**/ }
}
class Person: Codable {
/// The name of the person.
let name: String
/// The heterogeneous list of Pets
let pets: [Pet]
enum PersonCodingKeys: String, CodingKey {
case name
case pets
}
// CHALLENGE 1: Nested Heterogeneous Array Decoded.
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PersonCodingKeys.self)
name = try container.decode(String.self, forKey: .name)
pets = try container.decode(family: PetFamily.self, forKey: .pets)
}
// CHALLENGE 2: Heterogeneous Array as decoded return type.
func getPets(completion: ([Pet]) -> Void) throws {
let data = Data() // TODO: Replace this data with pet data.
completion(try JSONDecoder().decode(family: PetFamily.self, from: data))
}
}
/// To support a new class family, create an enum that conforms to this protocol and contains the different types.
protocol ClassFamily: Decodable {
/// The discriminator key.
static var discriminator: Discriminator { get }
/// Returns the class type of the object coresponding to the value.
func getType() -> AnyObject.Type
}
/// Discriminator key enum used to retrieve discriminator fields in JSON payloads.
enum Discriminator: String, CodingKey {
case type = "type"
}
/// The PetFamily enum describes the Pet family of objects.
enum PetFamily: String, ClassFamily {
case cat = "Cat"
case dog = "Dog"
static var discriminator: Discriminator = .type
func getType() -> AnyObject.Type {
switch self {
case .cat:
return Cat.self
case .dog:
return Dog.self
}
}
}
extension JSONDecoder {
/// Decode a heterogeneous list of objects.
/// - Parameters:
/// - family: The ClassFamily enum type to decode with.
/// - data: The data to decode.
/// - Returns: The list of decoded objects.
func decode<T: ClassFamily, U: Decodable>(family: T.Type, from data: Data) throws -> [U] {
return try self.decode([ClassWrapper<T, U>].self, from: data).compactMap { $0.object }
}
private class ClassWrapper<T: ClassFamily, U: Decodable>: Decodable {
/// The family enum containing the class information.
let family: T
/// The decoded object. Can be any subclass of U.
let object: U?
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Discriminator.self)
// Decode the family with the discriminator.
family = try container.decode(T.self, forKey: T.discriminator)
// Decode the object by initialising the corresponding type.
if let type = family.getType() as? U.Type {
object = try type.init(from: decoder)
} else {
object = nil
}
}
}
}
extension KeyedDecodingContainer {
/// Decode a heterogeneous list of objects for a given family.
/// - Parameters:
/// - family: The ClassFamily enum for the type family.
/// - key: The CodingKey to look up the list in the current container.
/// - Returns: The resulting list of heterogeneousType elements.
func decode<T : Decodable, U : ClassFamily>(family: U.Type, forKey key: K) throws -> [T] {
var container = try self.nestedUnkeyedContainer(forKey: key)
var list = [T]()
var tmpContainer = container
while !container.isAtEnd {
let typeContainer = try container.nestedContainer(keyedBy: Discriminator.self)
let family: U = try typeContainer.decode(U.self, forKey: U.discriminator)
if let type = family.getType() as? T.Type {
list.append(try tmpContainer.decode(type))
}
}
return list
}
}
// MARK: - EXAMPLES
let petsJson = """
[
{ "type": "Cat", "name": "Garfield", "lives": 9 },
{ "type": "Dog", "name": "Pluto" }
]
"""
let personJson = """
{
"name": "Kewin",
"pets": \(petsJson)
}
"""
if let personData = personJson.data(using: .utf8), let petsData = petsJson.data(using: .utf8) {
let decoder = JSONDecoder()
// Correctly decoded Person with pets.
let person = try? decoder.decode(Person.self, from: personData)
print("Correctly decoded Person with pets: \(person?.pets)") // Prints [Cat, Dog]
// Wrongly decoded Pets
let pets1 = decoder.decode([Pet].self, from: petsData)
print("Wrongly decoded pets: \(pets1)") // Prints [Pet, Pet]
// Correctly decoded Pets
let pets2: [Pet] = decoder.decode(family: PetFamily.self, from: petsData)
print("Correctly decoded pets: \(pets2)") // Prints [Cat, Dog]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment