Skip to content

Instantly share code, notes, and snippets.

@ollieatkinson
Last active September 17, 2022 12:13
Show Gist options
  • Save ollieatkinson/b38921bc8b38b0329af30bd945741113 to your computer and use it in GitHub Desktop.
Save ollieatkinson/b38921bc8b38b0329af30bd945741113 to your computer and use it in GitHub Desktop.
JSONDecoding
//: Playground - noun: a place where people can play
import Foundation
/// A type describing a JSON object.
public typealias JSONObject = Any
/// Provides information about an error which occured when trying to decode the JSON object.
public enum DecodingError: Error {
/// - invalidKeyPath: Thrown when the keyPath is not in the correct format.
case invalidKeyPath(keyPath: KeyPath)
/// - missing: Thrown when an item could not be found the JSON object.
case missing(keyPath: KeyPath, enclosing: JSONObject, reason: String)
/// - isNil: Thrown when the value of the specified key is nil.
case isNil(keyPath: KeyPath, reason: String)
/// - typeMismatch: Thrown when casting to `expected` fails or when trying to cast as [Key : Any].
case typeMismatch(keyPath: KeyPath, expected: Any.Type, actual: Any.Type, enclosing: JSONObject, reason: String)
}
/// A type describing the key used for indexing JSON objects.
public typealias Key = String
/// precedence group for the custom operators => and =>?
precedencegroup KeyPrecedence {
associativity: right
higherThan: CastingPrecedence
}
infix operator => : KeyPrecedence
infix operator =>? : KeyPrecedence
/// Return the value in dictionary `JSON` at keyPath `keyPath`
///
/// - parameter JSON: The dictionary object to query.
/// - parameter keyPath: A key path of the form _relationship.property_ (with one or more relationships); for example “department.name”
///
/// - throws: see DecodingError
///
/// - returns: The value for the derived property identified by keyPath.
public func => <Value: Any>(JSON: [Key : Any], keyPath: KeyPath) throws -> Value {
return try parse(JSON: JSON, keyPath: keyPath, type: Value.self)
}
/// Return the value in dictionary `JSON` at keyPath `keyPath`
///
/// - parameter JSON: The dictionary object to query.
/// - parameter keyPath: A key path of the form _relationship.property_ (with one or more relationships); for example “department.name”
///
/// - returns: The value from the JSON object for `key` or nil if failure.
public func =>? <Value: Any>(JSON: [Key : Any], keyPath: KeyPath) -> Value? {
return try? JSON => keyPath
}
/// Return the value in dictionary `JSON` at keyPath `keyPath`. If the value doesnt exist or is the wrong type a default value is used.
///
/// - parameter JSON: The dictionary object to query.
/// - parameter data: The keyPath and the default value.
///
/// - throws: see DecodingError
///
/// - returns: The value for the derived property identified by keyPath.
public func => <Value: Any>(JSON: [Key : Any], data: (keyPath: KeyPath, default: Value)) -> Value {
return (JSON =>? data.keyPath) ?? data.default
}
/// Return the value in dictionary `JSON` at keyPath `keyPath`
///
/// - parameter JSON: The dictionary object to query.
/// - parameter keyPath: A key path of the form _relationship.property_ (with one or more relationships); for example “department.name”
///
/// - throws: see DecodingError
///
/// - returns: The value for the derived property identified by keyPath.
public func => <Value: Any>(JSON: JSONObject, keyPath: KeyPath) throws -> Value {
return try parse(JSON: JSON, keyPath: keyPath, type: Value.self)
}
/// Return the value in dictionary `JSON` at keyPath `keyPath`
///
/// - parameter JSON: The dictionary object to query.
/// - parameter keyPath: A key path of the form _relationship.property_ (with one or more relationships); for example “department.name”
///
/// - returns: The value for the derived property identified by keyPath.
public func =>? <Value: Any>(JSON: JSONObject, keyPath: KeyPath) -> Value? {
return try? JSON => keyPath
}
/// Return the value in dictionary `JSON` at keyPath `keyPath`. If the value doesnt exist or is the wrong type a default value is used.
///
/// - parameter JSON: The dictionary object to query.
/// - parameter data: The keyPath and the default value.
///
/// - returns: The value for the derived property identified by keyPath.
public func => <Value: Any>(JSON: JSONObject, data: (keyPath: KeyPath, default: Value)) -> Value {
return (JSON =>? data.keyPath) ?? data.default
}
// MARK: Functions
func parse<Value: Any>(JSON: JSONObject, keyPath: KeyPath, type: Value.Type) throws -> Value {
guard let JSONData = JSON as? [Key : Any] else {
throw DecodingError.typeMismatch(
keyPath: keyPath,
expected: [Key : Any].self,
actual: type(of: JSON),
enclosing: JSON,
reason: "Could not unwrap JSONObject as \([Key : Any].self)"
)
}
return try parse(JSON: JSONData, keyPath: keyPath, type: type)
}
func parse<Value: Any>(JSON: [Key : Any], keyPath: KeyPath, type: Value.Type) throws -> Value {
guard !keyPath.isEmpty else {
throw DecodingError.invalidKeyPath(keyPath: keyPath)
}
var current = JSON
for (index, key) in keyPath.segments.dropLast().enumerated() {
guard let next = current[key] as? [Key : Any] else {
throw DecodingError.missing(
keyPath: keyPath,
enclosing: current as JSONObject,
reason: "\(keyPath.segments[0...index]) is missing in \(current)"
)
}
current = next
}
guard let key = keyPath.segments.last else {
throw DecodingError.invalidKeyPath(keyPath: keyPath)
}
guard current.keys.contains(key) else {
throw DecodingError.missing(keyPath: keyPath, enclosing: current as JSONObject, reason: "\(keyPath) is missing in \(current)")
}
guard let value = current[key], !(value is NSNull) else {
throw DecodingError.isNil(keyPath: keyPath, reason: "\(keyPath) is null in \(current)")
}
guard let unwrapped = value as? Value else {
throw DecodingError.typeMismatch(
keyPath: keyPath,
expected: Value.self,
actual: type(of: value),
enclosing: current as JSONObject,
reason: "\(type) for \(key) was specified but got type \(type(of: value))"
)
}
return unwrapped
}
/// A representation of a series of strings used for accessing the value in a dictionary. These strings represent a keyPath.
public struct KeyPath {
/// An array of `Key` objects used as a representation of a keyPath.
var segments: [Key]
/// A Boolean value indicating whether the KeyPath is empty.
var isEmpty: Bool {
return segments.isEmpty
}
/// The path for the keyPath, returned as an String object joined with fullstops '.'
var path: String {
return segments.joined(separator: ".")
}
}
/// A string representation of the key path shown as "this.is.a.keypath"
extension KeyPath: CustomStringConvertible {
public var description: String {
return path
}
}
/// Initializes a KeyPath with a string of the form "this.is.a.keypath"
extension KeyPath {
init(_ string: String) {
segments = string.components(separatedBy: ".")
}
}
/// Create a key path with a plain string literal like "this.is.a.key.path", without needing to explicitly create a KeyPath instance.
extension KeyPath: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value)
}
public init(unicodeScalarLiteral value: String) {
self.init(value)
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
}
/// An extension on dictionary to provide keyPath access.
/// e.g. data[keyPath: "a.b.c"] // == "value"
extension Dictionary {
subscript(keyPath keyPath: KeyPath) -> Any? {
return try? parse(JSON: self as JSONObject, keyPath: keyPath, type: Any.self)
}
}
@ollieatkinson
Copy link
Author

ollieatkinson commented Jan 14, 2017

var data: [String: Any] = [
    "this": [
        "b": "bValue",
        "is": [
            "a": "keyPath"
        ]
    ],
    "author": "Oliver Atkinson"
]

using the operator:

do {
    let value: String = try data => "this.b" //bValue
} catch let error {
    print(error)
}

using subscripting:

let value = data[keyPath: "this.is.a"] // keyPath

accessing a missing key:

do {
    let value: String = try data => "a.b"
} catch let error {
    print(error) // DecodingError.missing(a.b, dictionary, [ a ] is missing in <dictionary>)
}

do {
    let value: Int = try data => "this.c"
} catch let error {
    print(error)  // DecodingError.missing(this.c, dictionary, this.c is missing in <dictionary>)
}

accessing a key and casting the wrong type:

do {
    let value: Int = try data => "this.b"
} catch let error {
    print(error) // DecodingError.typeMismatch(this.b, expected: Swift.Int, actual: Swift.string, "Int for `b` was specified but got type String")
}

@ollieatkinson
Copy link
Author

ollieatkinson commented Jan 14, 2017

Getting back JSON data from a server, and indexing a keyPath:

let JSONData: Data = "{\"index\":{\"value\":42}}".data(using: .utf8)!

/// {
///    "index": {
///         "value": 42
///    }
/// }

do {
    let JSON = try JSONSerialization.jsonObject(with: JSONData, options: [ ])
    let value: Int = try JSON => "index.value" // 42
} catch {
    // failed
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment