Create a gist now

Instantly share code, notes, and snippets.

@jarsen /JaSON.swift
Last active Nov 10, 2016

What would you like to do?
JSON Value Extraction in Swift. Blog post here http://jasonlarsen.me/2015/10/16/no-magic-json-pt3.html
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)")
}
        if let _ = result as? NSNull {
            throw JSONError.NullValue(key: key)
        }

Needs to be moved from JSONValueForKey to anyForKey just above the return.

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