Skip to content

Instantly share code, notes, and snippets.

@benasher44
Last active October 22, 2020 13:48
Show Gist options
  • Save benasher44/38a20bb8fde1c8a1537575e36ef8e1c3 to your computer and use it in GitHub Desktop.
Save benasher44/38a20bb8fde1c8a1537575e36ef8e1c3 to your computer and use it in GitHub Desktop.
JSONObjectDecoder
//
// 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