Atelier Clockwork

Typed Returns

More Concise, More Swift-y.

EDIT: Thanks to my co-worker Eric Slosser for catching that my optional getter wasn't properly checking to see if the value was NSNULL() if the JSON included explicit null values returned by the server.

I was writing some code that had to parse a JSON blob, and quickly realized that I was writing the same checks over and over, particularly checking to see if a required key was nil, then if it was the correct type. After some fiddling, I managed to encapsulate a lot of that code into a pair of extensions on Dictionary.

import Foundation

enum JSONDecodingError: ErrorType {
    case NilValueForProperty
    case TypeMismatch
}

extension Dictionary where Key: StringLiteralConvertible, Value: AnyObject {

    // Adds a Swift-y way to get a non-optional type from a key
    func getValue<TypedReturn>(key: Key) throws -> TypedReturn {
        guard let valueObject = self[key] else {
            throw JSONDecodingError.NilValueForProperty
        }
        guard let value = valueObject as? TypedReturn else {
            throw JSONDecodingError.TypeMismatch
        }
        return value
    }


    // Adds a Swift-y way to get a optional type from a key
    func getOptionalValue<TypedReturn>(key: Key) throws -> TypedReturn? {
        if let valueObject = self[key] where valueObject !== NSNull() {
            guard let value = valueObject as? TypedReturn else {
                throw JSONDecodingError.TypeMismatch
            }
            return value
        }
        return nil
    }

}

let values: [String: AnyObject] = [
    "a": 2,
    "b" :"Test",
    "c": 4
]

do {
    let a: Int = try values.getValue("a")
}
catch {
    error
}

do {
    let b: Int = try values.getValue("b")
}
catch {
    error
}

do {
    let q: Int = try values.getValue("q")
}
catch {
    error
}

do {
    let q: Int? = try values.getOptionalValue("q")
}
catch {
    error
}

do {
    let b: Int? = try values.getOptionalValue("b")
}
catch {
    error
}

Copied and pasted into a playground, this will return values in the rows that have the correct type, error for rows that have the wrong type, and error for missing rows unless getOptionalValue is used. I wanted to use the same method signature and automatically swap implementations for Optional values, but I couldn't figure out how to constrain the first function to only apply to non optional values, and without that constraint the functions collide on any optional value.

There's a lot of interesting ways to tune the idea to make it better, but it made the code I was working on much more concise while still capturing error data to help with troubleshooting any errors coming in on the import.