Last active
November 10, 2016 00:14
-
-
Save jarsen/0f0919d3f43b2a2268e6 to your computer and use it in GitHub Desktop.
JSON Value Extraction in Swift. Blog post here http://jasonlarsen.me/2015/10/16/no-magic-json-pt3.html
This file contains 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: - JSONError Type | |
// | |
public enum JSONError: ErrorType, CustomStringConvertible { | |
case KeyNotFound(key: JSONKeyType) | |
case NullValue(key: JSONKeyType) | |
case TypeMismatch(expected: Any, actual: Any) | |
case TypeMismatchWithKey(key: JSONKeyType, expected: Any, actual: Any) | |
public var description: String { | |
switch self { | |
case let .KeyNotFound(key): | |
return "Key not found: \(key.stringValue)" | |
case let .NullValue(key): | |
return "Null Value found at: \(key.stringValue)" | |
case let .TypeMismatch(expected, actual): | |
return "Type mismatch. Expected type \(expected). Got '\(actual)'" | |
case let .TypeMismatchWithKey(key, expected, actual): | |
return "Type mismatch. Expected type \(expected) at key: \(key). Got '\(actual)'" | |
} | |
} | |
} | |
// | |
// MARK: - JSONKeyType | |
// | |
public protocol JSONKeyType: Hashable { | |
var stringValue: String { get } | |
} | |
extension String: JSONKeyType { | |
public var stringValue: String { | |
return self | |
} | |
} | |
// | |
// MARK: - JSONValueType | |
// | |
public protocol JSONValueType { | |
typealias ValueType = Self | |
static func JSONValue(object: Any) throws -> ValueType | |
} | |
extension JSONValueType { | |
public static func JSONValue(object: Any) throws -> ValueType { | |
guard let objectValue = object as? ValueType else { | |
throw JSONError.TypeMismatch(expected: ValueType.self, actual: object.dynamicType) | |
} | |
return objectValue | |
} | |
} | |
// | |
// MARK: - JSONValueType Implementations | |
// | |
extension String: JSONValueType {} | |
extension Int: JSONValueType {} | |
extension UInt: JSONValueType {} | |
extension Float: JSONValueType {} | |
extension Double: JSONValueType {} | |
extension Bool: JSONValueType {} | |
extension Array where Element: JSONValueType { | |
public static func JSONValue(object: Any) throws -> [Element] { | |
guard let anyArray = object as? [AnyObject] else { | |
throw JSONError.TypeMismatch(expected: self, actual: object.dynamicType) | |
} | |
return try anyArray.map { try Element.JSONValue($0) as! Element } | |
} | |
} | |
extension Dictionary: JSONValueType { | |
public static func JSONValue(object: Any) throws -> [Key: Value] { | |
guard let objectValue = object as? [Key: Value] else { | |
throw JSONError.TypeMismatch(expected: self, actual: object.dynamicType) | |
} | |
return objectValue | |
} | |
} | |
extension NSURL: JSONValueType { | |
public static func JSONValue(object: Any) throws -> NSURL { | |
guard let urlString = object as? String, objectValue = NSURL(string: urlString) else { | |
throw JSONError.TypeMismatch(expected: self, actual: object.dynamicType) | |
} | |
return objectValue | |
} | |
} | |
// | |
// MARK: - JSONObjectConvertible | |
// | |
public protocol JSONObjectConvertible : JSONValueType { | |
typealias ConvertibleType = Self | |
init(json: JSONObject) throws | |
} | |
extension JSONObjectConvertible { | |
public static func JSONValue(object: Any) throws -> ConvertibleType { | |
guard let json = object as? JSONObject else { | |
throw JSONError.TypeMismatch(expected: JSONObject.self, actual: object.dynamicType) | |
} | |
guard let value = try self.init(json: json) as? ConvertibleType else { | |
throw JSONError.TypeMismatch(expected: ConvertibleType.self, actual: object.dynamicType) | |
} | |
return value | |
} | |
} | |
// | |
// MARK: - JSONObject | |
// | |
public typealias JSONObject = [String: AnyObject] | |
extension Dictionary where Key: JSONKeyType { | |
private func anyForKey(key: Key) throws -> Any { | |
let pathComponents = key.stringValue.characters.split(".").map(String.init) | |
var accumulator: Any = self | |
for component in pathComponents { | |
if let componentData = accumulator as? [Key: Value], value = componentData[component as! Key] { | |
accumulator = value | |
continue | |
} | |
throw JSONError.KeyNotFound(key: key) | |
} | |
if let _ = accumulator as? NSNull { | |
throw JSONError.NullValue(key: key) | |
} | |
return accumulator | |
} | |
public func JSONValueForKey<A: JSONValueType>(key: Key) throws -> A { | |
let any = try anyForKey(key) | |
guard let result = try A.JSONValue(any) as? A else { | |
throw JSONError.TypeMismatchWithKey(key: key, expected: A.self, actual: any.dynamicType) | |
} | |
return result | |
} | |
public func JSONValueForKey<A: JSONValueType>(key: Key) throws -> [A] { | |
let any = try anyForKey(key) | |
return try Array<A>.JSONValue(any) | |
} | |
public func JSONValueForKey<A: JSONValueType>(key: Key) throws -> A? { | |
do { | |
return try self.JSONValueForKey(key) as A | |
} | |
catch JSONError.KeyNotFound { | |
return nil | |
} | |
catch JSONError.NullValue { | |
return nil | |
} | |
catch { | |
throw error | |
} | |
} | |
} | |
// | |
// MARK: - Tests | |
// | |
struct User : JSONObjectConvertible { | |
let name: String | |
let email: String | |
init(json: JSONObject) throws { | |
name = try json.JSONValueForKey("name") | |
email = try json.JSONValueForKey("email") | |
} | |
} | |
var json: JSONObject = ["url": "http://apple.com", "foo": (2 as NSNumber), "str": "Hello, World!", "array": [1,2,3,4,7], "object": ["foo": (3 as NSNumber), "str": "Hello, World!"], "bool": (true as NSNumber), "urls": ["http://apple.com", "http://google.com"], "user": ["name": "Jason", "email": "email@email.com"], "users": [["name": "Jason", "email": "email@email.com"], ["name": "Bob", "email": "bob@email.com"]]] | |
do { | |
var str: String = try json.JSONValueForKey("str") | |
// var foo1: String = try json.JSONValueForKey("foo") | |
var foo2: Int = try json.JSONValueForKey("foo") | |
var foo3: Int? = try json.JSONValueForKey("foo") | |
var foo4: Int? = try json.JSONValueForKey("bar") | |
var arr: [Int] = try json.JSONValueForKey("array") | |
var obj: JSONObject? = try json.JSONValueForKey("object") | |
let innerfoo: Int = try obj!.JSONValueForKey("foo") | |
let innerfoo2: Int = try json.JSONValueForKey("object.foo") | |
let bool: Bool = try json.JSONValueForKey("bool") | |
let url: NSURL = try json.JSONValueForKey("url") | |
let urls: [NSURL] = try json.JSONValueForKey("urls") | |
let user: User = try json.JSONValueForKey("user") | |
let users: [User] = try json.JSONValueForKey("users") | |
} | |
catch { | |
print("\(error)") | |
} |
You should add support for optional arrays:
public func JSONValueForKey<A: JSONValueType>(key: Key) throws -> [A]? {
do {
return try self.JSONValueForKey(key) as [A]
}
catch JSONError.KeyNotFound {
return nil
}
catch JSONError.NullValue {
return nil
}
catch {
throw error
}
}
So, you can do the following without an error being thrown:
let oUrls: [NSURL]? = try json.JSONValueForKey("optionalUrls")
line 54:
throw JSONError.TypeMismatch(expected: JSONValue.self, actual: object.dynamicType)
should be:
throw JSONError.TypeMismatch(expected: ValueType.self, actual: object.dynamicType)
Proposal:
Add case InvalidValue(value: Any)
and case InvalidValueWithKey(key: JSONKeyType, value: Any)
to JSONError
. This is useful in situations in which a value is transformed into another type, such as when converting a String
to NSURL
.
Example:
extension NSURL: JSONValueType {
public static func JSONValue(object: Any) throws -> NSURL {
guard let urlString = object as? String else {
throw JSONError.TypeMismatch(expected: self, actual: object.dynamicType)
}
guard let objectValue = NSURL(string: urlString) else {
throw JSONError.InvalidValue(value: urlString)
}
return objectValue
}
}
This allows for a more accurate error when the type is actually the expected type (String
), but the value is invalid for constructing the new type. In my case, I got a URL string that had a space at the end. There was not a type mismatch, but an invalid value. However, the error thrown was JSONError.TypeMismatch
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Needs to be moved from
JSONValueForKey
toanyForKey
just above the return.