├── .circleci └── config.yml ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── Codability.podspec ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Codability │ ├── AnyCodable.swift │ ├── GenericDecodingContainer.swift │ ├── InvalidElementStrategy.swift │ ├── KeyedDecodingContainer+Any.swift │ ├── KeyedEncodingContainer+Any.swift │ └── RawCodingKey.swift └── Tests ├── CodabilityTests ├── AnyCodableTests.swift ├── InvalidElementTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | macos: 5 | xcode: "9.3.0" 6 | steps: 7 | - checkout 8 | - run: swift build 9 | - run: swift test 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Master 4 | 5 | ## 0.2.1 6 | 7 | #### Fixed 8 | - Fix nill AnyCodable values being encoded as null 9 | 10 | [Commits](https://github.com/yonaskolb/XcodeGen/compare/0.2.0...0.2.1) 11 | 12 | ## 0.2.0 13 | 14 | #### Added 15 | - `InvalidElementBehaviour` for controlling what happens when a collection element fails decoding 16 | 17 | [Commits](https://github.com/yonaskolb/XcodeGen/compare/0.1.0...0.2.0) 18 | 19 | ## 0.1.0 20 | Initial release 21 | -------------------------------------------------------------------------------- /Codability.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Codability' 3 | s.version = '0.2.1' 4 | s.swift_version = '4.1' 5 | s.summary = 'Useful helpers for working with Codable types in Swift' 6 | s.homepage = 'http://github.com/yonaskolb/Codability' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'Yonas Kolb' => 'yonas4596@hotmail.com' } 9 | s.social_media_url = 'https://twitter.com/yonaskolb' 10 | 11 | s.ios.deployment_target = '9.0' 12 | s.osx.deployment_target = '10.9' 13 | s.tvos.deployment_target = '9.0' 14 | s.watchos.deployment_target = '3.0' 15 | 16 | s.source = { :git => 'https://github.com/yonaskolb/Codability.git', :tag => s.version.to_s } 17 | s.source_files = 'Sources/**/*.swift' 18 | s.frameworks = 'Foundation' 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yonas Kolb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Codability", 7 | products: [ 8 | .library(name: "Codability", targets: ["Codability"]), 9 | ], 10 | targets: [ 11 | .target(name: "Codability"), 12 | .testTarget(name: "CodabilityTests", dependencies: ["Codability"]), 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codability 2 | 3 | [![SPM](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=for-the-badge)](https://swift.org/package-manager) 4 | [![Git Version](https://img.shields.io/github/release/yonaskolb/Codability.svg?style=for-the-badge)](https://github.com/yonaskolb/Codability/releases) 5 | [![Build Status](https://img.shields.io/circleci/project/github/yonaskolb/Codability.svg?style=for-the-badge)](https://circleci.com/gh/yonaskolb/Codability) 6 | [![license](https://img.shields.io/github/license/yonaskolb/Codability.svg?style=for-the-badge)](https://github.com/yonaskolb/Codability/blob/master/LICENSE) 7 | 8 | Useful helpers for working with `Codable` types in Swift 9 | 10 | - [Invalid Element Strategy](#invalid-element-strategy) 11 | - [Any Codable](#any-codable) 12 | - [Raw CodingKey](#raw-codingkey) 13 | - [Generic Decoding Functions](#generic-decoding-functions) 14 | 15 | ## Installing 16 | 17 | ### Swift Package Manager 18 | 19 | Add the following to your `Package.swift` dependencies: 20 | 21 | ```swift 22 | .package(url: "https://github.com/yonaskolb/Codability.git", from: "0.2.0"), 23 | ``` 24 | 25 | And then import wherever needed: `import Codability` 26 | 27 | ## Helpers 28 | 29 | ### Invalid Element Strategy 30 | By default Decodable will throw an error if a single element within an array or dictionary fails. `InvalidElementStrategy` is an enum that lets you control this behaviour. It has multiple cases: 31 | 32 | - `remove`: Removes the element from the collection 33 | - `fail`: Will fail the whole decoding. This is the default behaviour used by the decoders 34 | - `fallback(value)`: This lets you provide a typed value in a type safe way 35 | - `custom((EncodingError)-> InvalidElementStrategy)`: Lets you provide dynamic behaviour depending on the specific error that was throw which lets you lookup exactly which keys were involved 36 | 37 | When decoding an array or dictionary use the `decodeArray` and `decodeDictionary` functions (there are also `IfPresent` variants as well). 38 | 39 | The `InvalidElementStrategy` can either be passed into these functions, or a default can be set using `JSONDecoder().userInfo[.invalidElementStrategy]`, otherwise the default of `fail` will be used. 40 | 41 | Given the following JSON: 42 | 43 | ```json 44 | { 45 | "array": [1, "two", 3], 46 | "dictionary": { 47 | "one": 1, 48 | "two": "two", 49 | "three": 3 50 | } 51 | } 52 | ``` 53 | ```swift 54 | struct Object: Decodable { 55 | 56 | let array: [Int] 57 | let dictionary: [String: Int] 58 | 59 | public init(from decoder: Decoder) throws { 60 | let container = try decoder.container(keyedBy: RawCodingKey.self) 61 | array = try container.decodeArray([Int].self, forKey: "array", invalidElementStrategy: .fallback(0)) 62 | dictionary = try container.decodeDictionary([String: Int].self, forKey: "dictionary", invalidElementStrategy: .remove) 63 | } 64 | } 65 | ``` 66 | 67 | ```swift 68 | let decoder = JSONDecoder() 69 | 70 | // this will provide a default if none is passed into the decode functions 71 | decoder.userInfo[.invalidElementStrategy] = InvalidElementStrategy.remove 72 | 73 | let decodedObject = try decoder.decode(Object.self, from: json) 74 | decodedObject.array == [1,0,3] 75 | decodedObject.dictionary = ["one": 1, "three": 3] 76 | ``` 77 | 78 | ### Any Codable 79 | The downside of using Codable is that you can't encode and decode properties where the type is mixed or unknown, for example `[String: Any]`, `[Any]` or `Any`. 80 | These are sometimes a neccessary evil in many apis, and `AnyCodable` makes supporting these types easy. 81 | 82 | There are 2 few different way to use it: 83 | 84 | #### As a Codable property 85 | The advantage of this is you can use the synthesized codable functions. 86 | The downside though is that these values must be unwrapped using `AnyCodable.value`. You can add custom setters and getters on your objects to make accessing these easier though 87 | 88 | ```swift 89 | struct AnyContainer: Codable { 90 | let dictionary: [String: AnyCodable] 91 | let array: [AnyCodable] 92 | let value: AnyCodable 93 | } 94 | ``` 95 | 96 | #### Custom decoding and encoding functions 97 | 98 | This lets you keep your normal structures, but requires using the `decodeAny` or `encodeAny` functions. If you have to implement a custom `init(from:)` or `encode` function for other reasons, this is the way to go. Behind the scenes this uses `AnyCodable` to do the coding, and then does a cast to your expect type in the case of decoding. 99 | 100 | ```swift 101 | struct AnyContainer: Codable { 102 | let dictionary: [String: Any] 103 | let array: [Any] 104 | let value: Any 105 | 106 | public init(from decoder: Decoder) throws { 107 | let container = try decoder.container(keyedBy: CodingKeys.self) 108 | 109 | dictionary = try container.decodeAny(.dictionary) 110 | array = try container.decodeAny([Any].self, forKey: .array) 111 | value = try container.decodeAny(Any.self, forKey: .value) 112 | } 113 | 114 | func encode(to encoder: Encoder) throws { 115 | var container = encoder.container(keyedBy: CodingKeys.self) 116 | try container.encodeAny(dictionary, forKey: .dictionary) 117 | try container.encodeAny(array, forKey: .array) 118 | try container.encodeAny(value, forKey: .value) 119 | } 120 | 121 | enum CodingKeys: CodingKey { 122 | case dictionary 123 | case value 124 | case array 125 | } 126 | } 127 | ``` 128 | 129 | ### Raw CodingKey 130 | `RawCodingKey` can be used to provide dynamic coding keys. It also remove the need to create the standard `CodingKey` enum when you are only using those values in once place. 131 | 132 | ```swift 133 | struct Object: Decodable { 134 | 135 | let int: Int 136 | let bool: Bool 137 | 138 | public init(from decoder: Decoder) throws { 139 | let container = try decoder.container(keyedBy: RawCodingKey.self) 140 | int = try container.decode(Int.self, forKey: "int") 141 | bool = try container.decode(Bool.self, forKey: "bool") 142 | } 143 | } 144 | ``` 145 | 146 | ### Generic Decoding functions 147 | The default decoding functions on `KeyedDecodingContainer` and `UnkeyedDecodingContainer` all require an explicity type to be passed in. `Codabilty` adds generic functions to remove the need for this, making your `init(from:)` much cleaner. The `key` parameter also becomes unnamed. 148 | 149 | All the helper functions provided by `Codabality` such as the `decodeAny`, `decodeArray` or `decodeDictionary` functions also have these generic variants including `IfPresent`. 150 | 151 | ```swift 152 | struct Object: Decodable { 153 | 154 | let int: Int? 155 | let bool: Bool 156 | 157 | public init(from decoder: Decoder) throws { 158 | let container = try decoder.container(keyedBy: RawCodingKey.self) 159 | 160 | // old 161 | int = try container.decodeIfPresent(Int.self, forKey: "int") 162 | bool = try container.decode(Bool.self, forKey: "bool") 163 | 164 | // new 165 | int = try container.decodeIfPresent("int") 166 | bool = try container.decode("bool") 167 | } 168 | } 169 | 170 | ``` 171 | 172 | ## Other Codability helpers 173 | [JohnSundell/Codextended](https://github.com/JohnSundell/Codextended) 174 | 175 | ## Attributions 176 | Thanks to @mattt and [Flight-School/AnyCodable](https://github.com/Flight-School/AnyCodable) for the basis of `AnyCodable` support. [License](https://github.com/Flight-School/AnyCodable/blob/master/LICENSE.md) 177 | -------------------------------------------------------------------------------- /Sources/Codability/AnyCodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Based on https://github.com/Flight-School/AnyCodable 4 | 5 | /** 6 | A type-erased `Codable` value. 7 | 8 | You can encode or decode mixed-type or unknown values in dictionaries 9 | and other collections that require `Encodable` or `Decodable` conformance 10 | by declaring their contained type to be `AnyCodable`. 11 | */ 12 | public struct AnyCodable { 13 | public let value: Any 14 | 15 | public init(_ value: T?) { 16 | self.value = value ?? () 17 | } 18 | } 19 | 20 | extension AnyCodable: Codable { 21 | 22 | public init(from decoder: Decoder) throws { 23 | let container = try decoder.singleValueContainer() 24 | 25 | if container.decodeNil() { 26 | self.init(()) 27 | } else if let bool = try? container.decode(Bool.self) { 28 | self.init(bool) 29 | } else if let int = try? container.decode(Int.self) { 30 | self.init(int) 31 | } else if let uint = try? container.decode(UInt.self) { 32 | self.init(uint) 33 | } else if let double = try? container.decode(Double.self) { 34 | self.init(double) 35 | } else if let string = try? container.decode(String.self) { 36 | self.init(string) 37 | } else if let array = try? container.decode([AnyCodable].self) { 38 | self.init(array.map { $0.value }) 39 | } else if let dictionary = try? container.decode([String: AnyCodable].self) { 40 | self.init(dictionary.mapValues { $0.value }) 41 | } else { 42 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyCodable value cannot be decoded") 43 | } 44 | } 45 | 46 | public func encode(to encoder: Encoder) throws { 47 | var container = encoder.singleValueContainer() 48 | 49 | switch self.value { 50 | case is Void: 51 | try container.encodeNil() 52 | case let bool as Bool: 53 | try container.encode(bool) 54 | case let int as Int: 55 | try container.encode(int) 56 | case let int8 as Int8: 57 | try container.encode(int8) 58 | case let int16 as Int16: 59 | try container.encode(int16) 60 | case let int32 as Int32: 61 | try container.encode(int32) 62 | case let int64 as Int64: 63 | try container.encode(int64) 64 | case let uint as UInt: 65 | try container.encode(uint) 66 | case let uint8 as UInt8: 67 | try container.encode(uint8) 68 | case let uint16 as UInt16: 69 | try container.encode(uint16) 70 | case let uint32 as UInt32: 71 | try container.encode(uint32) 72 | case let uint64 as UInt64: 73 | try container.encode(uint64) 74 | case let float as Float: 75 | try container.encode(float) 76 | case let double as Double: 77 | try container.encode(double) 78 | case let string as String: 79 | try container.encode(string) 80 | case let date as Date: 81 | try container.encode(date) 82 | case let url as URL: 83 | try container.encode(url) 84 | case let array as [Any?]: 85 | try container.encode(array.map { AnyCodable($0) }) 86 | case let dictionary as [String: Any?]: 87 | try container.encode(dictionary.mapValues { AnyCodable($0) }) 88 | default: 89 | let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyCodable value cannot be encoded") 90 | throw EncodingError.invalidValue(self.value, context) 91 | } 92 | } 93 | } 94 | 95 | extension AnyCodable: Equatable { 96 | public static func ==(lhs: AnyCodable, rhs: AnyCodable) -> Bool { 97 | switch (lhs.value, rhs.value) { 98 | case is (Void, Void): 99 | return true 100 | case let (lhs as Bool, rhs as Bool): 101 | return lhs == rhs 102 | case let (lhs as Int, rhs as Int): 103 | return lhs == rhs 104 | case let (lhs as Int8, rhs as Int8): 105 | return lhs == rhs 106 | case let (lhs as Int16, rhs as Int16): 107 | return lhs == rhs 108 | case let (lhs as Int32, rhs as Int32): 109 | return lhs == rhs 110 | case let (lhs as Int64, rhs as Int64): 111 | return lhs == rhs 112 | case let (lhs as UInt, rhs as UInt): 113 | return lhs == rhs 114 | case let (lhs as UInt8, rhs as UInt8): 115 | return lhs == rhs 116 | case let (lhs as UInt16, rhs as UInt16): 117 | return lhs == rhs 118 | case let (lhs as UInt32, rhs as UInt32): 119 | return lhs == rhs 120 | case let (lhs as UInt64, rhs as UInt64): 121 | return lhs == rhs 122 | case let (lhs as Float, rhs as Float): 123 | return lhs == rhs 124 | case let (lhs as Double, rhs as Double): 125 | return lhs == rhs 126 | case let (lhs as String, rhs as String): 127 | return lhs == rhs 128 | case (let lhs as [String: AnyCodable], let rhs as [String: AnyCodable]): 129 | return lhs == rhs 130 | case (let lhs as [AnyCodable], let rhs as [AnyCodable]): 131 | return lhs == rhs 132 | default: 133 | return false 134 | } 135 | } 136 | } 137 | 138 | extension AnyCodable: CustomStringConvertible { 139 | public var description: String { 140 | switch value { 141 | case is Void: 142 | return String(describing: nil as Any?) 143 | case let value as CustomStringConvertible: 144 | return value.description 145 | default: 146 | return String(describing: value) 147 | } 148 | } 149 | } 150 | 151 | extension AnyCodable: CustomDebugStringConvertible { 152 | public var debugDescription: String { 153 | switch value { 154 | case let value as CustomDebugStringConvertible: 155 | return "AnyCodable(\(value.debugDescription))" 156 | default: 157 | return "AnyCodable(\(self.description))" 158 | } 159 | } 160 | } 161 | 162 | extension AnyCodable: ExpressibleByNilLiteral, ExpressibleByBooleanLiteral, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByStringLiteral, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral { 163 | 164 | public init(nilLiteral: ()) { 165 | self.init(nil as Any?) 166 | } 167 | 168 | public init(booleanLiteral value: Bool) { 169 | self.init(value) 170 | } 171 | 172 | public init(integerLiteral value: Int) { 173 | self.init(value) 174 | } 175 | 176 | public init(floatLiteral value: Double) { 177 | self.init(value) 178 | } 179 | 180 | public init(extendedGraphemeClusterLiteral value: String) { 181 | self.init(value) 182 | } 183 | 184 | public init(stringLiteral value: String) { 185 | self.init(value) 186 | } 187 | 188 | public init(arrayLiteral elements: Any...) { 189 | self.init(elements) 190 | } 191 | 192 | public init(dictionaryLiteral elements: (AnyHashable, Any)...) { 193 | self.init(Dictionary(elements, uniquingKeysWith: { (first, _) in first })) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Sources/Codability/GenericDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension KeyedDecodingContainer { 4 | 5 | // codable 6 | public func decode(_ key: KeyedDecodingContainer.Key) throws -> T where T: Decodable { 7 | return try decode(T.self, forKey: key) 8 | } 9 | 10 | public func decodeIfPresent(_ key: KeyedDecodingContainer.Key) throws -> T? where T: Decodable { 11 | return try decodeIfPresent(T.self, forKey: key) 12 | } 13 | 14 | // any 15 | public func decodeAny(_ key: K) throws -> T { 16 | return try decodeAny(T.self, forKey: key) 17 | } 18 | 19 | public func decodeAnyIfPresent(_ key: K) throws -> T? { 20 | return try decodeAnyIfPresent(T.self, forKey: key) 21 | } 22 | 23 | // array 24 | public func decodeArray(_ key: K, invalidElementStrategy: InvalidElementStrategy? = nil) throws -> [T] { 25 | return try decodeArray([T].self, forKey: key, invalidElementStrategy: invalidElementStrategy) 26 | } 27 | 28 | public func decodeArrayIfPresent(_ key: K, invalidElementStrategy: InvalidElementStrategy? = nil) throws -> [T]? { 29 | return try decodeArrayIfPresent([T].self, forKey: key, invalidElementStrategy: invalidElementStrategy) 30 | } 31 | 32 | // dictionary 33 | public func decodeDictionary(_ key: K, invalidElementStrategy: InvalidElementStrategy? = nil) throws -> [String: T] { 34 | return try decodeDictionary([String: T].self, forKey: key, invalidElementStrategy: invalidElementStrategy) 35 | } 36 | 37 | public func decodeDictionaryIfPresent(_ key: K, invalidElementStrategy: InvalidElementStrategy? = nil) throws -> [String: T]? { 38 | return try decodeDictionaryIfPresent([String: T].self, forKey: key, invalidElementStrategy: invalidElementStrategy) 39 | } 40 | } 41 | 42 | extension UnkeyedDecodingContainer { 43 | 44 | mutating func decode() throws -> T { 45 | return try decode(T.self) 46 | } 47 | 48 | mutating func decodeIfPresent() throws -> T? { 49 | return try decodeIfPresent(T.self) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Codability/InvalidElementStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvalidElementDecodingStrategy.swift 3 | // Codability 4 | // 5 | // Created by Yonas Kolb on 30/4/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum InvalidElementStrategy: CustomStringConvertible { 11 | case remove 12 | case fail 13 | case fallback(T) 14 | case custom((DecodingError) -> InvalidElementStrategy) 15 | 16 | func decodeItem(onError: ((Error) -> ())? = nil, decode: () throws -> T) throws -> T? { 17 | do { 18 | return try decode() 19 | } catch { 20 | onError?(error) 21 | switch self { 22 | case .remove: 23 | return nil 24 | case .fail: 25 | throw error 26 | case let .fallback(value): 27 | return value 28 | case let .custom(getBehaviour): 29 | guard let decodingError = error as? DecodingError else { throw error } 30 | let behaviour = getBehaviour(decodingError) 31 | return try behaviour.decodeItem(onError: onError, decode: decode) 32 | } 33 | } 34 | } 35 | 36 | func toType() -> InvalidElementStrategy { 37 | switch self { 38 | case .remove: 39 | return .remove 40 | case .fail: 41 | return .fail 42 | case let .fallback(value): 43 | if let value = value as? T { 44 | return .fallback(value) 45 | } else { 46 | return .fail 47 | } 48 | case let .custom(getBehaviour): 49 | return .custom( { error in 50 | getBehaviour(error).toType() 51 | }) 52 | } 53 | } 54 | 55 | public var description: String { 56 | switch self { 57 | case .remove: 58 | return "remove" 59 | case .fail: 60 | return "fail" 61 | case .fallback: 62 | return "fallback" 63 | case .custom: 64 | return "custom" 65 | } 66 | } 67 | } 68 | 69 | extension KeyedDecodingContainer { 70 | 71 | public func decodeArray(_ type: [T].Type, forKey key: K, invalidElementStrategy: InvalidElementStrategy? = nil) throws -> [T] { 72 | var container = try nestedUnkeyedContainer(forKey: key) 73 | var chosenInvalidElementStrategy: InvalidElementStrategy 74 | if let invalidElementStrategy = invalidElementStrategy { 75 | chosenInvalidElementStrategy = invalidElementStrategy 76 | } else if let invalidElementStrategy = try superDecoder().userInfo[.invalidElementStrategy] as? InvalidElementStrategy { 77 | chosenInvalidElementStrategy = invalidElementStrategy.toType() 78 | } else { 79 | chosenInvalidElementStrategy = .fail 80 | } 81 | 82 | var array: [T] = [] 83 | while !container.isAtEnd { 84 | let element: T? = try chosenInvalidElementStrategy.decodeItem(onError: { _ in 85 | // hack to advance the current index 86 | _ = try? container.decode(AnyCodable.self) 87 | }) { 88 | try container.decode(T.self) 89 | } 90 | if let element = element { 91 | array.append(element) 92 | } 93 | } 94 | return array 95 | } 96 | 97 | public func decodeArrayIfPresent(_ type: [T].Type, forKey key: K, invalidElementStrategy: InvalidElementStrategy? = nil) throws -> [T]? { 98 | if !contains(key) { 99 | return nil 100 | } 101 | return try decodeArray(type, forKey: key, invalidElementStrategy: invalidElementStrategy) 102 | } 103 | 104 | public func decodeDictionary(_ type: [String: T].Type, forKey key: K, invalidElementStrategy: InvalidElementStrategy? = nil) throws -> [String: T] { 105 | let container = try self.nestedContainer(keyedBy: RawCodingKey.self, forKey: key) 106 | 107 | var chosenInvalidElementStrategy: InvalidElementStrategy 108 | if let invalidElementStrategy = invalidElementStrategy { 109 | chosenInvalidElementStrategy = invalidElementStrategy 110 | } else if let invalidElementStrategy = try superDecoder().userInfo[.invalidElementStrategy] as? InvalidElementStrategy { 111 | chosenInvalidElementStrategy = invalidElementStrategy.toType() 112 | } else { 113 | chosenInvalidElementStrategy = .fail 114 | } 115 | 116 | var dictionary: [String: T] = [:] 117 | for key in container.allKeys { 118 | 119 | let element: T? = try chosenInvalidElementStrategy.decodeItem { 120 | try container.decode(T.self, forKey: key) 121 | } 122 | if let element = element { 123 | dictionary[key.stringValue] = element 124 | } 125 | } 126 | return dictionary 127 | } 128 | 129 | public func decodeDictionaryIfPresent(_ type: [String: T].Type, forKey key: K, invalidElementStrategy: InvalidElementStrategy? = nil) throws -> [String: T]? { 130 | if !contains(key) { 131 | return nil 132 | } 133 | return try decodeDictionary(type, forKey: key, invalidElementStrategy: invalidElementStrategy) 134 | } 135 | } 136 | 137 | extension CodingUserInfoKey { 138 | static let invalidElementStrategy = CodingUserInfoKey(rawValue: "invalidElementStrategy")! 139 | } 140 | -------------------------------------------------------------------------------- /Sources/Codability/KeyedDecodingContainer+Any.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension KeyedDecodingContainer { 4 | 5 | public func decodeAny(_ type: T.Type, forKey key: K) throws -> T { 6 | guard let value = try decode(AnyCodable.self, forKey: key).value as? T else { 7 | throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Decoding of \(T.self) failed")) 8 | } 9 | return value 10 | } 11 | 12 | public func decodeAnyIfPresent(_ type: T.Type, forKey key: K) throws -> T? { 13 | if !contains(key) { 14 | return nil 15 | } 16 | 17 | return try decodeAny(type, forKey: key) 18 | } 19 | 20 | public func toDictionary() throws -> [String: Any] { 21 | var dictionary: [String: Any] = [:] 22 | for key in allKeys { 23 | dictionary[key.stringValue] = try decodeAny(key) 24 | } 25 | return dictionary 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Codability/KeyedEncodingContainer+Any.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | extension KeyedEncodingContainer { 5 | 6 | public mutating func encodeAnyIfPresent(_ value: T?, forKey key: K) throws { 7 | guard let value = value else { return } 8 | try encodeIfPresent(AnyCodable(value), forKey: key) 9 | } 10 | 11 | public mutating func encodeAny(_ value: T, forKey key: K) throws { 12 | try encode(AnyCodable(value), forKey: key) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Codability/RawCodingKey.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | public struct RawCodingKey: CodingKey { 5 | 6 | private let string: String 7 | private let int: Int? 8 | 9 | public var stringValue: String { return string } 10 | 11 | public init(string: String) { 12 | self.string = string 13 | int = nil 14 | } 15 | 16 | public init?(stringValue: String) { 17 | string = stringValue 18 | int = nil 19 | } 20 | 21 | public var intValue: Int? { return int } 22 | public init?(intValue: Int) { 23 | string = String(describing: intValue) 24 | int = intValue 25 | } 26 | } 27 | 28 | extension RawCodingKey: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral { 29 | 30 | public init(stringLiteral value: String) { 31 | string = value 32 | int = nil 33 | } 34 | 35 | public init(integerLiteral value: Int) { 36 | string = "" 37 | int = value 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/CodabilityTests/AnyCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import Codability 4 | 5 | final class AnyContainerTests: XCTestCase { 6 | 7 | let jsonString = """ 8 | { 9 | "boolean": true, 10 | "integer": 1, 11 | "double": 3.2, 12 | "string": "string", 13 | "array": [1, 2, 3], 14 | "nested": { 15 | "a": "alpha", 16 | "b": "bravo", 17 | "c": "charlie" 18 | } 19 | } 20 | """ 21 | 22 | let jsonValue: [String: Any] = [ 23 | "boolean": true, 24 | "integer": 1, 25 | "double": 3.2, 26 | "string": "string", 27 | "array": [1, 2, 3], 28 | "nested": [ 29 | "a": "alpha", 30 | "b": "bravo", 31 | "c": "charlie" 32 | ] 33 | ] 34 | 35 | let object = Object(boolean: true, integer: 1, double: 3.2, string: "string", array: [1,2,3]) 36 | 37 | lazy var anyJson = """ 38 | { 39 | "dictionary": \(jsonString), 40 | "array": [ 41 | "hello", 42 | 2, 43 | true 44 | ], 45 | "value": true 46 | } 47 | """ 48 | 49 | func testDecoding() { 50 | let json = jsonString.data(using: .utf8)! 51 | 52 | let decoder = JSONDecoder() 53 | let dictionary = try! decoder.decode([String: AnyCodable].self, from: json) 54 | 55 | XCTAssertEqual(dictionary["boolean"]?.value as! Bool, true) 56 | XCTAssertEqual(dictionary["integer"]?.value as! Int, 1) 57 | XCTAssertEqual(dictionary["double"]?.value as! Double, 3.2) 58 | XCTAssertEqual(dictionary["string"]?.value as! String, "string") 59 | XCTAssertEqual(dictionary["array"]?.value as! [Int], [1, 2, 3]) 60 | XCTAssertEqual(dictionary["nested"]?.value as! [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"]) 61 | } 62 | 63 | func testEncoding() { 64 | let dictionary: [String: AnyCodable] = [ 65 | "boolean": true, 66 | "integer": 1, 67 | "double": 3.2, 68 | "string": "string", 69 | "array": [1, 2, 3], 70 | "nested": [ 71 | "a": "alpha", 72 | "b": "bravo", 73 | "c": "charlie" 74 | ] 75 | ] 76 | 77 | let encoder = JSONEncoder() 78 | 79 | let json = try! encoder.encode(dictionary) 80 | let encodedJSONObject = try! JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary 81 | 82 | let expected = jsonString.data(using: .utf8)! 83 | let expectedJSONObject = try! JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary 84 | 85 | XCTAssertEqual(encodedJSONObject, expectedJSONObject) 86 | } 87 | 88 | func testObjectCoding() throws { 89 | 90 | let json = jsonString.data(using: .utf8)! 91 | let decoder = JSONDecoder() 92 | let encoder = JSONEncoder() 93 | let decodedObject = try! decoder.decode(Object.self, from: json) 94 | let encodedObject = try encoder.encode(decodedObject) 95 | let decodedEncodedObject = try! decoder.decode(Object.self, from: encodedObject) 96 | 97 | XCTAssertEqual(object, decodedObject) 98 | XCTAssertEqual(decodedObject, decodedEncodedObject) 99 | } 100 | 101 | func testCodingAny() throws { 102 | 103 | let json = anyJson.data(using: .utf8)! 104 | 105 | let decoder = JSONDecoder() 106 | let object = try decoder.decode(AnyContainer.self, from: json) 107 | 108 | func assert(_ object: AnyContainer) { 109 | XCTAssertEqual(object.dictionary["boolean"] as! Bool, true) 110 | XCTAssertEqual(object.dictionary["integer"] as! Int, 1) 111 | XCTAssertEqual(object.dictionary["double"] as! Double, 3.2) 112 | XCTAssertEqual(object.dictionary["string"] as! String, "string") 113 | XCTAssertEqual(object.dictionary["array"] as! [Int], [1, 2, 3]) 114 | XCTAssertEqual(object.dictionary["nested"] as! [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"]) 115 | XCTAssertEqual(object.array[0] as! String, "hello") 116 | XCTAssertEqual(object.array[1] as! Int, 2) 117 | XCTAssertEqual(object.array[2] as! Bool, true) 118 | XCTAssertEqual(object.value as! Bool, true) 119 | } 120 | assert(object) 121 | 122 | let encoder = JSONEncoder() 123 | let data = try encoder.encode(object) 124 | let object2 = try decoder.decode(AnyContainer.self, from: data) 125 | assert(object2) 126 | } 127 | 128 | func testToDictionary() throws { 129 | let json = jsonString.data(using: .utf8)! 130 | let decoder = JSONDecoder() 131 | let decodedObject = try! decoder.decode(DictionaryObject.self, from: json) 132 | XCTAssertTrue((decodedObject.properties as NSDictionary).isEqual(to: jsonValue)) 133 | } 134 | 135 | 136 | static var allTests = [ 137 | ("testCodingAny", testCodingAny), 138 | ] 139 | } 140 | 141 | struct DictionaryObject: Decodable { 142 | let properties: [String: Any] 143 | public init(from decoder: Decoder) throws { 144 | let container = try decoder.container(keyedBy: RawCodingKey.self) 145 | properties = try container.toDictionary() 146 | } 147 | } 148 | 149 | struct Object: Codable, Equatable { 150 | 151 | let boolean: Bool 152 | let integer: Int 153 | let double: Double 154 | let string: String 155 | let array: [Int] 156 | 157 | init(boolean: Bool, integer: Int, double: Double, string: String, array: [Int]) { 158 | self.boolean = boolean 159 | self.integer = integer 160 | self.double = double 161 | self.string = string 162 | self.array = array 163 | } 164 | 165 | public init(from decoder: Decoder) throws { 166 | let container = try decoder.container(keyedBy: RawCodingKey.self) 167 | 168 | boolean = try container.decode("boolean") 169 | integer = try container.decode("integer") 170 | double = try container.decode("double") 171 | string = try container.decode("string") 172 | array = try container.decode("array") 173 | } 174 | 175 | func encode(to encoder: Encoder) throws { 176 | var container = encoder.container(keyedBy: RawCodingKey.self) 177 | try container.encode(boolean, forKey: "boolean") 178 | try container.encode(integer, forKey: "integer") 179 | try container.encode(double, forKey: "double") 180 | try container.encode(string, forKey: "string") 181 | try container.encode(array, forKey: "array") 182 | } 183 | } 184 | 185 | struct AnyContainer: Codable { 186 | 187 | let dictionary: [String: Any] 188 | let array: [Any] 189 | let value: Any 190 | 191 | public init(from decoder: Decoder) throws { 192 | let container = try decoder.container(keyedBy: CodingKeys.self) 193 | 194 | dictionary = try container.decodeAny([String: Any].self, forKey: .dictionary) 195 | array = try container.decodeAny([Any].self, forKey: .array) 196 | value = try container.decodeAny(Any.self, forKey: .value) 197 | } 198 | 199 | func encode(to encoder: Encoder) throws { 200 | var container = encoder.container(keyedBy: CodingKeys.self) 201 | try container.encodeAny(dictionary, forKey: .dictionary) 202 | try container.encodeAny(array, forKey: .array) 203 | try container.encodeAny(value, forKey: .value) 204 | } 205 | 206 | enum CodingKeys: CodingKey { 207 | case dictionary 208 | case value 209 | case array 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Tests/CodabilityTests/InvalidElementTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvalidElementTests.swift 3 | // CodabilityTests 4 | // 5 | // Created by Yonas Kolb on 30/4/18. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import Codability 11 | 12 | final class InvalidElementTests: XCTestCase { 13 | 14 | let json = """ 15 | { 16 | "array": [1, "two", 3], 17 | "dictionary": { 18 | "one": 1, 19 | "two": "two", 20 | "three": 3 21 | } 22 | } 23 | """.data(using: .utf8)! 24 | 25 | struct DefaultStrategyObject: Decodable { 26 | 27 | let array: [Int] 28 | let dictionary: [String: Int] 29 | 30 | public init(from decoder: Decoder) throws { 31 | let container = try decoder.container(keyedBy: RawCodingKey.self) 32 | array = try container.decodeArray([Int].self, forKey: "array") 33 | dictionary = try container.decodeDictionary([String: Int].self, forKey: "dictionary") 34 | } 35 | } 36 | 37 | struct ExplicitStrategyObject: Decodable { 38 | 39 | let array: [Int] 40 | let dictionary: [String: Int] 41 | 42 | public init(from decoder: Decoder) throws { 43 | let container = try decoder.container(keyedBy: RawCodingKey.self) 44 | array = try container.decodeArray([Int].self, forKey: "array", invalidElementStrategy: .remove) 45 | dictionary = try container.decodeDictionary([String: Int].self, forKey: "dictionary", invalidElementStrategy: .remove) 46 | } 47 | } 48 | 49 | func testDefaultInvalidElementStrategy() throws { 50 | 51 | let decoder = JSONDecoder() 52 | 53 | decoder.userInfo[.invalidElementStrategy] = InvalidElementStrategy.remove 54 | let decodedObject = try decoder.decode(DefaultStrategyObject.self, from: json) 55 | XCTAssertEqual(decodedObject.array, [1, 3]) 56 | XCTAssertEqual(decodedObject.dictionary, ["one": 1, "three": 3]) 57 | 58 | decoder.userInfo[.invalidElementStrategy] = InvalidElementStrategy.fallback(2) 59 | let decodedObject2 = try decoder.decode(DefaultStrategyObject.self, from: json) 60 | XCTAssertEqual(decodedObject2.array, [1, 2, 3]) 61 | XCTAssertEqual(decodedObject2.dictionary, ["one": 1, "two": 2, "three": 3]) 62 | 63 | decoder.userInfo[.invalidElementStrategy] = InvalidElementStrategy.fail 64 | XCTAssertThrowsError(_ = try decoder.decode(DefaultStrategyObject.self, from: json)) 65 | 66 | decoder.userInfo[.invalidElementStrategy] = InvalidElementStrategy.custom({ _ in .remove }) 67 | let decodedObject3 = try decoder.decode(DefaultStrategyObject.self, from: json) 68 | XCTAssertEqual(decodedObject3.array, [1, 3]) 69 | XCTAssertEqual(decodedObject3.dictionary, ["one": 1, "three": 3]) 70 | } 71 | 72 | func testExplictInvalidElementStrategy() throws { 73 | 74 | let decoder = JSONDecoder() 75 | 76 | let decodedObject = try decoder.decode(ExplicitStrategyObject.self, from: json) 77 | XCTAssertEqual(decodedObject.array, [1, 3]) 78 | XCTAssertEqual(decodedObject.dictionary, ["one": 1, "three": 3]) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/CodabilityTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(AnyContainerTests.allTests), 7 | testCase(AnyDecodableTests.allTests), 8 | testCase(AnyEncodableTests.allTests), 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import CodabilityTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += CodabilityTests.allTests() 7 | XCTMain(tests) --------------------------------------------------------------------------------