Last active
October 22, 2020 13:48
-
-
Save benasher44/38a20bb8fde1c8a1537575e36ef8e1c3 to your computer and use it in GitHub Desktop.
JSONObjectDecoder
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
// | |
// Created by Ben Asher on 12/18/17. | |
// Copyright © 2017-present PlanGrid. All rights reserved. | |
// | |
import Foundation | |
/// A JSON decoder for decoding a Foundation-typed JSON object generated by `JSONSerialization` | |
/// into concrete swift `Decodable` types | |
public final class JSONObjectDecoder { | |
/// The top level JSON value being decoded | |
/// | |
/// - array: An array of values | |
/// - dict: A dictionary of values | |
/// - singleValue: A single value (for passing a decoder to a `Decodable` that expects to access | |
/// `Decoder.singleValueContainer()`) | |
private enum Value { | |
case array([Any]) | |
case dict([String: Any]) | |
case singleValue(Any) | |
} | |
private let jsonValue: Value | |
public let codingPath: [CodingKey] | |
public let userInfo: [CodingUserInfoKey: Any] = [:] | |
/// Initializes a decoder to decode a JSON dictionary | |
/// | |
/// - Parameters: | |
/// - dict: the dictionary to decode | |
/// - codingPath: the current path into the JSON object | |
public init(_ dict: [String: Any], codingPath: [CodingKey] = []) { | |
self.jsonValue = .dict(dict) | |
self.codingPath = codingPath | |
} | |
/// Initializes a decoder to decode a JSON array | |
/// | |
/// - Parameters: | |
/// - dict: the array to decode | |
/// - codingPath: the current path into the JSON object | |
public init(_ array: [Any], codingPath: [CodingKey] = []) { | |
self.jsonValue = .array(array) | |
self.codingPath = codingPath | |
} | |
/// Initializes a decoder to parse any value. This is useful for initializing a decoder to parse | |
/// any nested value | |
/// | |
/// - Parameters: | |
/// - anyValue: The value to decode | |
/// - codingPath: The current path into the JSON value | |
private init(_ anyValue: Any, codingPath: [CodingKey] = []) { | |
switch anyValue { | |
case let anyValue as [Any]: | |
self.jsonValue = .array(anyValue) | |
case let anyValue as [String: Any]: | |
self.jsonValue = .dict(anyValue) | |
default: | |
self.jsonValue = .singleValue(anyValue) | |
} | |
self.codingPath = codingPath | |
} | |
/// Attempts to decode the json object in the decoder to the return type | |
/// | |
/// - Returns: The decoded object | |
/// - Throws: `DecodingError` | |
public func decode<T: Decodable>() throws -> T { | |
return try T(from: self) | |
} | |
/// Attempts to decode an array of object concurrently | |
/// | |
/// - Parameter errors: Any errors encountered decoding individual objects | |
/// - Returns: An Array of decoded objects | |
/// - Throws: `DecodingError`, if the top level element in not an array | |
public func concurrentDecode<T: Decodable>(errors: inout [Error]) throws -> [T] { | |
guard case let .array(values) = self.jsonValue else { | |
throw DecodingError.typeMismatch( | |
[Any].self, | |
DecodingError.Context( | |
codingPath: self.codingPath, | |
debugDescription: "Expected array for current value; found \(self.jsonValue))" | |
) | |
) | |
} | |
var resultArray = [T]() | |
resultArray.reserveCapacity(values.count) | |
var lock = os_unfair_lock() | |
// We can use an UnsafeBufferPointer to skip bounds checking | |
values.withUnsafeBufferPointer { unsafeValues in | |
DispatchQueue.concurrentPerform(iterations: values.count) { i in | |
let value = unsafeValues[i] | |
let codingPath = self.codingPath + [IntKey(i)] | |
do { | |
let transformedValue = try T(from: JSONObjectDecoder(value, codingPath: codingPath)) | |
os_unfair_lock_lock(&lock) | |
defer { os_unfair_lock_unlock(&lock) } | |
resultArray.append(transformedValue) | |
} catch { | |
os_unfair_lock_lock(&lock) | |
defer { os_unfair_lock_unlock(&lock) } | |
errors.append(error) | |
} | |
} | |
} | |
return resultArray | |
} | |
} | |
// MARK: - Shared | |
/// An integer-based key that represents a position in a JSON array for the `codingPath` | |
private struct IntKey: CodingKey { | |
private let value: Int | |
init(_ intValue: Int) { | |
self.value = intValue | |
} | |
// CodingKey Methods | |
var stringValue: String { return String(self.value) } | |
init?(stringValue: String) { | |
guard let value = Int(stringValue) else { return nil } | |
self.init(value) | |
} | |
var intValue: Int? { return self.value } | |
init?(intValue: Int) { | |
self.init(intValue) | |
} | |
} | |
// MARK: - Decoder Methods | |
extension JSONObjectDecoder: Decoder { | |
public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey { | |
guard case let .dict(value) = self.jsonValue else { | |
throw DecodingError.typeMismatch( | |
[String: Any].self, | |
DecodingError.Context( | |
codingPath: self.codingPath, | |
debugDescription: "Expected dict for current value; found \(self.jsonValue))" | |
) | |
) | |
} | |
return KeyedDecodingContainer( | |
JSONObjectDecoder.KeyedContainer<Key>(value, codingPath: self.codingPath) | |
) | |
} | |
public func unkeyedContainer() throws -> UnkeyedDecodingContainer { | |
guard case let .array(value) = self.jsonValue else { | |
throw DecodingError.typeMismatch( | |
[Any].self, | |
DecodingError.Context( | |
codingPath: self.codingPath, | |
debugDescription: "Expected array for current value; found \(self.jsonValue))" | |
) | |
) | |
} | |
return JSONObjectDecoder.UnkeyedContainer(value, codingPath: self.codingPath) | |
} | |
public func singleValueContainer() throws -> SingleValueDecodingContainer { | |
let value: Any | |
switch self.jsonValue { | |
case let .array(anyValue): | |
value = anyValue | |
case let .dict(anyValue): | |
value = anyValue | |
case let .singleValue(anyValue): | |
value = anyValue | |
} | |
return JSONObjectDecoder.SingleValueContainer(value, codingPath: self.codingPath) | |
} | |
} | |
// MARK: - JSONObjectDecoder's Container types | |
extension JSONObjectDecoder { | |
struct KeyedContainer<JsonKey: CodingKey> { | |
let codingPath: [CodingKey] | |
private let jsonValue: [String: Any] | |
init(_ jsonValue: [String: Any], codingPath: [CodingKey]) { | |
self.codingPath = codingPath | |
self.jsonValue = jsonValue | |
} | |
} | |
struct UnkeyedContainer { | |
let codingPath: [CodingKey] | |
var currentIndex: Int = 0 | |
private let jsonValue: [Any] | |
init(_ jsonValue: [Any], codingPath: [CodingKey]) { | |
self.codingPath = codingPath | |
self.jsonValue = jsonValue | |
} | |
} | |
struct SingleValueContainer { | |
private let jsonValue: Any | |
let codingPath: [CodingKey] | |
init(_ jsonValue: Any, codingPath: [CodingKey]) { | |
self.codingPath = codingPath | |
self.jsonValue = jsonValue | |
} | |
} | |
} | |
// MARK: - Container Conformances | |
extension JSONObjectDecoder.KeyedContainer: KeyedDecodingContainerProtocol { | |
/// Gets the value for the key and attempts to cast it to the appropriate primitive type | |
private func value<T>(forKey key: CodingKey) throws -> T { | |
// Attempt to get a value for the specified key | |
guard let value = self.jsonValue[key.stringValue] else { | |
throw DecodingError.valueNotFound( | |
T.self, | |
DecodingError.Context( | |
codingPath: self.codingPath, | |
debugDescription: "Value missing for key \(key)" | |
) | |
) | |
} | |
// Attempt to cast the value to the specified return type | |
guard let typedValue = value as? T else { | |
throw DecodingError.typeMismatch( | |
T.self, | |
DecodingError.Context( | |
codingPath: self.codingPath, | |
debugDescription: "Expected \(T.self); found \(type(of: value))" | |
) | |
) | |
} | |
return typedValue | |
} | |
var allKeys: [JsonKey] { | |
return self.jsonValue.keys.compactMap { JsonKey(stringValue: $0) } | |
} | |
func contains(_ key: JsonKey) -> Bool { | |
return (self.jsonValue[key.stringValue] != nil) | |
} | |
func decodeNil(forKey key: JsonKey) throws -> Bool { | |
guard let value = self.jsonValue[key.stringValue] else { return true } | |
return (value is NSNull) | |
} | |
func decode(_ type: Bool.Type, forKey key: JsonKey) throws -> Bool { return try self.value(forKey: key) } | |
func decode(_ type: Int.Type, forKey key: JsonKey) throws -> Int { return try self.value(forKey: key) } | |
func decode(_ type: Int8.Type, forKey key: JsonKey) throws -> Int8 { return try self.value(forKey: key) } | |
func decode(_ type: Int16.Type, forKey key: JsonKey) throws -> Int16 { return try self.value(forKey: key) } | |
func decode(_ type: Int32.Type, forKey key: JsonKey) throws -> Int32 { return try self.value(forKey: key) } | |
func decode(_ type: Int64.Type, forKey key: JsonKey) throws -> Int64 { return try self.value(forKey: key) } | |
func decode(_ type: UInt.Type, forKey key: JsonKey) throws -> UInt { return try self.value(forKey: key) } | |
func decode(_ type: UInt8.Type, forKey key: JsonKey) throws -> UInt8 { return try self.value(forKey: key) } | |
func decode(_ type: UInt16.Type, forKey key: JsonKey) throws -> UInt16 { return try self.value(forKey: key) } | |
func decode(_ type: UInt32.Type, forKey key: JsonKey) throws -> UInt32 { return try self.value(forKey: key) } | |
func decode(_ type: UInt64.Type, forKey key: JsonKey) throws -> UInt64 { return try self.value(forKey: key) } | |
func decode(_ type: Float.Type, forKey key: JsonKey) throws -> Float { return try self.value(forKey: key) } | |
func decode(_ type: Double.Type, forKey key: JsonKey) throws -> Double { return try self.value(forKey: key) } | |
func decode(_ type: String.Type, forKey key: JsonKey) throws -> String { return try self.value(forKey: key) } | |
func decode<T>(_ type: T.Type, forKey key: JsonKey) throws -> T where T: Decodable { | |
let anyValue: Any = try self.value(forKey: key) | |
return try T(from: JSONObjectDecoder(anyValue, codingPath: self.codingPath + [key])) | |
} | |
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: JsonKey) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey { | |
let nestedValue: [String: Any] = try self.value(forKey: key) | |
return KeyedDecodingContainer( | |
JSONObjectDecoder.KeyedContainer<NestedKey>(nestedValue, codingPath: self.codingPath + [key]) | |
) | |
} | |
func nestedUnkeyedContainer(forKey key: JsonKey) throws -> UnkeyedDecodingContainer { | |
let nestedValue: [Any] = try self.value(forKey: key) | |
return JSONObjectDecoder.UnkeyedContainer(nestedValue, codingPath: self.codingPath + [key]) | |
} | |
func superDecoder() throws -> Decoder { | |
return JSONObjectDecoder(self.jsonValue, codingPath: self.codingPath) | |
} | |
func superDecoder(forKey key: JsonKey) throws -> Decoder { | |
guard let value = self.jsonValue[key.stringValue] else { | |
throw DecodingError.valueNotFound( | |
Any.self, | |
DecodingError.Context( | |
codingPath: self.codingPath, | |
debugDescription: "Value missing for key \(key)" | |
) | |
) | |
} | |
return JSONObjectDecoder(value, codingPath: self.codingPath + [key]) | |
} | |
} | |
extension JSONObjectDecoder.UnkeyedContainer: UnkeyedDecodingContainer { | |
var count: Int? { | |
return self.jsonValue.count | |
} | |
var isAtEnd: Bool { | |
return (self.currentIndex == self.count) | |
} | |
mutating func decodeNil() throws -> Bool { | |
let value = self.jsonValue[self.currentIndex] | |
let isNil = (value is NSNull) | |
if isNil { | |
self.currentIndex += 1 | |
} | |
return isNil | |
} | |
/// Attempts to decode the next value (incrementing self.currentIndex) and cast it to the | |
/// appropriate primitive type | |
private mutating func nextValue<T>() throws -> T { | |
defer { self.currentIndex += 1 } | |
let value = self.jsonValue[self.currentIndex] | |
guard let typedValue = value as? T else { | |
throw DecodingError.typeMismatch( | |
T.self, | |
DecodingError.Context( | |
codingPath: self.codingPath, | |
debugDescription: "Expected \(T.self); found \(type(of: value))" | |
) | |
) | |
} | |
return typedValue | |
} | |
mutating func decode(_ type: Bool.Type) throws -> Bool { return try self.nextValue() } | |
mutating func decode(_ type: Int.Type) throws -> Int { return try self.nextValue() } | |
mutating func decode(_ type: Int8.Type) throws -> Int8 { return try self.nextValue() } | |
mutating func decode(_ type: Int16.Type) throws -> Int16 { return try self.nextValue() } | |
mutating func decode(_ type: Int32.Type) throws -> Int32 { return try self.nextValue() } | |
mutating func decode(_ type: Int64.Type) throws -> Int64 { return try self.nextValue() } | |
mutating func decode(_ type: UInt.Type) throws -> UInt { return try self.nextValue() } | |
mutating func decode(_ type: UInt8.Type) throws -> UInt8 { return try self.nextValue() } | |
mutating func decode(_ type: UInt16.Type) throws -> UInt16 { return try self.nextValue() } | |
mutating func decode(_ type: UInt32.Type) throws -> UInt32 { return try self.nextValue() } | |
mutating func decode(_ type: UInt64.Type) throws -> UInt64 { return try self.nextValue() } | |
mutating func decode(_ type: Float.Type) throws -> Float { return try self.nextValue() } | |
mutating func decode(_ type: Double.Type) throws -> Double { return try self.nextValue() } | |
mutating func decode(_ type: String.Type) throws -> String { return try self.nextValue() } | |
mutating func decode<T>(_ type: T.Type) throws -> T where T: Decodable { | |
defer { self.currentIndex += 1 } | |
let value = self.jsonValue[self.currentIndex] | |
return try T(from: JSONObjectDecoder(value, codingPath: self.codingPath + [IntKey(self.currentIndex)])) | |
} | |
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey { | |
let index = self.currentIndex | |
let nestedValue: [String: Any] = try self.nextValue() | |
return KeyedDecodingContainer( | |
JSONObjectDecoder.KeyedContainer<NestedKey>(nestedValue, codingPath: self.codingPath + [IntKey(index)]) | |
) | |
} | |
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { | |
let index = self.currentIndex | |
let nestedValue: [Any] = try self.nextValue() | |
return JSONObjectDecoder.UnkeyedContainer(nestedValue, codingPath: self.codingPath + [IntKey(index)]) | |
} | |
mutating func superDecoder() throws -> Decoder { | |
return JSONObjectDecoder(self.jsonValue, codingPath: self.codingPath) | |
} | |
} | |
extension JSONObjectDecoder.SingleValueContainer: SingleValueDecodingContainer { | |
func decodeNil() -> Bool { | |
return (self.jsonValue is NSNull) | |
} | |
/// Casts the value to the appropriate primitive type | |
private func value<T>() throws -> T { | |
guard let typedValue = self.jsonValue as? T else { | |
throw DecodingError.typeMismatch( | |
T.self, | |
DecodingError.Context( | |
codingPath: self.codingPath, | |
debugDescription: "Expected \(T.self); found \(type(of: self.jsonValue))" | |
) | |
) | |
} | |
return typedValue | |
} | |
func decode(_ type: Bool.Type) throws -> Bool { return try self.value() } | |
func decode(_ type: Int.Type) throws -> Int { return try self.value() } | |
func decode(_ type: Int8.Type) throws -> Int8 { return try self.value() } | |
func decode(_ type: Int16.Type) throws -> Int16 { return try self.value() } | |
func decode(_ type: Int32.Type) throws -> Int32 { return try self.value() } | |
func decode(_ type: Int64.Type) throws -> Int64 { return try self.value() } | |
func decode(_ type: UInt.Type) throws -> UInt { return try self.value() } | |
func decode(_ type: UInt8.Type) throws -> UInt8 { return try self.value() } | |
func decode(_ type: UInt16.Type) throws -> UInt16 { return try self.value() } | |
func decode(_ type: UInt32.Type) throws -> UInt32 { return try self.value() } | |
func decode(_ type: UInt64.Type) throws -> UInt64 { return try self.value() } | |
func decode(_ type: Float.Type) throws -> Float { return try self.value() } | |
func decode(_ type: Double.Type) throws -> Double { return try self.value() } | |
func decode(_ type: String.Type) throws -> String { return try self.value() } | |
func decode<T>(_ type: T.Type) throws -> T where T: Decodable { | |
return try T(from: JSONObjectDecoder(self.jsonValue)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment