Last active
February 21, 2018 22:55
-
-
Save NSExceptional/69014874caa51785406bfd7b04e4b4aa to your computer and use it in GitHub Desktop.
A concise example of deserializing a JSON response into a model object with as little "glue code" as possible, and some runtime type checking to avoid BAD_INSTRUCTION.
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
// Tanner Bennett 2016 | |
import Foundation | |
typealias JSON = [String: Any] | |
/// For joining dictionaries; contents of `right` take preceedence over `left` | |
func + <K,V> (left: Dictionary<K,V>, right: Dictionary<K,V>?) -> Dictionary<K,V> { | |
guard let right = right else { return left } | |
var left = left | |
right.forEach { key, value in | |
left[key] = value | |
} | |
return left | |
} | |
/// Base class implements JSON parsing logic | |
/// Must inherit from NSObject to be able to use setValue(_:forKey:) | |
class JSONModel: NSObject { | |
enum JSONError: Error { | |
case typeMismatch(String) | |
} | |
init(json: JSON) throws { | |
super.init() | |
for (property, key) in type(of: self).propertyToJSONKeyPaths { | |
// Don't set nil values to allow for default values | |
if let value = self.parse(keyPath: key, in: json) { | |
let propertyType = try self.basicType(of: property) | |
let valueType = basicTypeof(value) | |
if propertyType == valueType { | |
self.setValue(value, forKey: property) | |
} else { | |
var message = "Cannot set property `\(type(of: self)).\(property)` of type `\(propertyType)` " | |
message += "to value of type `\(valueType)`:\n\(value)" | |
throw JSONError.typeMismatch(message) | |
} | |
} | |
} | |
} | |
/// Fetches an optional value from a JSON dictionary given a key path, like "foo.bar.baz" | |
private func parse(keyPath: String, in json: JSON) -> Any? { | |
guard !keyPath.isEmpty else { | |
return nil | |
} | |
// Separate "foo.bar.baz" into ["foo", "bar"] and "baz" | |
var keys = keyPath.components(separatedBy: ".") | |
let last = keys.popLast()! | |
var current: JSON? = json | |
// After this, `current` will be json["foo"]["bar"] | |
for key in keys { | |
if let previous = current { | |
current = previous[key] as? JSON | |
} else { | |
return nil | |
} | |
} | |
// json["foo"]["bar"]["baz"] | |
return current?[last] | |
} | |
/// Subclasses must override | |
class var propertyToJSONKeyPaths: [String: String] { | |
return [:] | |
} | |
} | |
/// Base class of custom class hierarchy, no initializer needed | |
class Foo: JSONModel { | |
private(set) var name: String? = nil | |
private(set) var id: UInt = 0 | |
private(set) var kind: String = "default" | |
override class var propertyToJSONKeyPaths: [String: String] { | |
return ["name": "user.name", | |
"id": "identifier", | |
"kind": "user.kind"] | |
} | |
} | |
class Bar: Foo { | |
private(set) var password: String? = nil | |
override class var propertyToJSONKeyPaths: [String: String] { | |
return super.propertyToJSONKeyPaths + ["password": "user.info.password"] | |
} | |
override var description: String { | |
return "\(self.name ?? "no name"), \(self.id), \(self.kind), \(self.password ?? "no password")" | |
} | |
} |
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
var json: JSON = ["identifier": 5, | |
"user": ["name": "bob", | |
"info": ["password": "p4ssw0rd"]]] | |
// bob, 5, default, p4ssw0rd | |
var bob = try? Bar(json: json) | |
do { | |
json = ["user": ["name": 5]] | |
test = try Bar(json: json) | |
} catch { | |
// Cannot set property 'Bar.name' of type 'object' to value of type 'integer': | |
// 5 | |
print(error) | |
} |
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
// Tanner Bennett 2016 | |
import ObjectiveC | |
enum BasicType { | |
case object, integer, float, unsupported | |
} | |
func basicTypeof(_ thing: Any) -> BasicType { | |
if isObject(thing: thing) { | |
return .object | |
} | |
if isInteger(thing: thing) { | |
return .integer | |
} | |
if isFloat(thing: thing) { | |
return .float | |
} | |
return .unsupported | |
} | |
/// "Object" being anything that can be safely assigned or converted to an Objc object | |
func isObject(thing: Any) -> Bool { | |
let type = Mirror(reflecting: thing).displayStyle | |
return type == .class || type == .dictionary || type == .set || | |
thing is String || thing is Array<Any> | |
} | |
/// Integer includes all integral scalar values such as Bool | |
func isInteger(thing: Any) -> Bool { | |
return (thing is Int || | |
thing is Int64 || | |
thing is Int32 || | |
thing is Int16 || | |
thing is Int8 || | |
thing is UInt || | |
thing is UInt64 || | |
thing is UInt32 || | |
thing is UInt16 || | |
thing is UInt8 || | |
thing is Bool | |
) | |
} | |
func isFloat(thing: Any) -> Bool { | |
return thing is Float || thing is Float32 || thing is Float64 || thing is Double | |
} | |
enum IntrospectionError: Error { | |
case propertyNotFound(String) | |
} | |
extension NSObject { | |
private static let objs: Set<Character> = ["@", "#"] | |
private static let ints: Set<Character> = ["c", "i", "s", "l", "q", "C", "I", "S", "L", "Q", "B"] | |
private static let floats: Set<Character> = ["f", "d"] | |
private func objcTypeOf(property: String) throws -> Character { | |
guard let objcProperty = class_getProperty(type(of: self), property) else { | |
let error = "Property `\(property)` does not exist or is incompatible with the ObjC runtime" | |
throw IntrospectionError.propertyNotFound(error) | |
} | |
// This line of code took me 20 minutes to write, I swear. | |
// The documentation for UnicodeScalar says it takes Int8 through Int64, I don't understand. | |
return Character(UnicodeScalar(Int(property_copyAttributeValue(objcProperty, "T")[0]))!) | |
} | |
func basicType(of property: String) throws -> BasicType { | |
let type = try self.objcTypeOf(property: property) | |
if NSObject.objs.contains(type) { | |
return .object | |
} | |
if NSObject.ints.contains(type) { | |
return .integer | |
} | |
if NSObject.floats.contains(type) { | |
return .float | |
} | |
return .unsupported | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment