Last active
February 13, 2022 21:27
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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