Skip to content

Instantly share code, notes, and snippets.

@NSExceptional
Last active February 21, 2018 22:55
Show Gist options
  • Save NSExceptional/69014874caa51785406bfd7b04e4b4aa to your computer and use it in GitHub Desktop.
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.
// 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")"
}
}
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)
}
// 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