Last active
September 17, 2022 12:13
-
-
Save ollieatkinson/b38921bc8b38b0329af30bd945741113 to your computer and use it in GitHub Desktop.
JSONDecoding
This file contains hidden or 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
//: 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) | |
} | |
} |
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
using the operator:
using subscripting:
accessing a missing key:
accessing a key and casting the wrong type: