├── Advanced Codable.playground ├── playground.xcworkspace │ └── contents.xcworkspacedata ├── contents.xcplayground └── Pages │ ├── 2.2 Changing Hierarchies.xcplaygroundpage │ └── Contents.swift │ ├── 1. Basic Example.xcplaygroundpage │ └── Contents.swift │ ├── 2.1 Changing Hierarchies .xcplaygroundpage │ └── Contents.swift │ ├── 4. Property Wrappers.xcplaygroundpage │ └── Contents.swift │ ├── 3.1 Heterogeneous Arrays.xcplaygroundpage │ └── Contents.swift │ └── 3.2 Heterogeneous Arrays - Refactor.xcplaygroundpage │ └── Contents.swift ├── LICENSE └── README.md /Advanced Codable.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Advanced Codable.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ben Scheirman 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 | -------------------------------------------------------------------------------- /Advanced Codable.playground/Pages/2.2 Changing Hierarchies.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let json = """ 4 | { 5 | "id": 123, 6 | "name": "Endeavor", 7 | "brewery_id": "sa001", 8 | "brewery_name": "Saint Arnold" 9 | } 10 | """.data(using: .utf8)! 11 | 12 | struct Brewery: Decodable { 13 | let id: String 14 | let name: String 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case id = "brewery_id" 18 | case name = "brewery_name" 19 | } 20 | } 21 | 22 | struct Beer: Decodable { 23 | let id: Int 24 | let name: String 25 | let brewery: Brewery 26 | 27 | enum CodingKeys: String, CodingKey { 28 | case id 29 | case name 30 | } 31 | 32 | init(from decoder: Decoder) throws { 33 | let container = try decoder.container(keyedBy: CodingKeys.self) 34 | id = try container.decode(Int.self, forKey: .id) 35 | name = try container.decode(String.self, forKey: .name) 36 | brewery = try Brewery(from: decoder) 37 | } 38 | } 39 | 40 | let decoder = JSONDecoder() 41 | let beer = try! decoder.decode(Beer.self, from: json) 42 | 43 | dump(beer) 44 | -------------------------------------------------------------------------------- /Advanced Codable.playground/Pages/1. Basic Example.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | struct Beer: Codable { 5 | let id: Int 6 | let name: String 7 | let brewery: String 8 | 9 | enum CodingKeys: String, CodingKey { 10 | case id 11 | case name 12 | case brewery 13 | } 14 | 15 | init(from decoder: Decoder) throws { 16 | let container = try decoder.container(keyedBy: CodingKeys.self) 17 | self.id = try container.decode(Int.self, forKey: .id) 18 | self.name = try container.decode(String.self, forKey: .name) 19 | self.brewery = try container.decode(String.self, forKey: .brewery) 20 | } 21 | 22 | func encode(to encoder: Encoder) throws { 23 | var container = encoder.container(keyedBy: CodingKeys.self) 24 | try container.encode(id, forKey: .id) 25 | try container.encode(name, forKey: .name) 26 | try container.encode(brewery, forKey: .brewery) 27 | } 28 | } 29 | 30 | let beerJSON = """ 31 | { 32 | "id": 123, 33 | "name": "Endeavor", 34 | "brewery": "Saint Arnold" 35 | } 36 | """.data(using: .utf8)! 37 | 38 | let decoder = JSONDecoder() 39 | let beer = try decoder.decode(Beer.self, from: beerJSON) 40 | 41 | dump(beer) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Codable 2 | 3 | This repo contains an Xcode playground of examples demonstrating customizing Codable for different scenarios. 4 | 5 | The playground has these pages: 6 | 7 | - 1. Basic Example 8 | 9 | This shows the basic Codable conformance that the compiler synthesizes for you. 10 | 11 | - 2.1 Changing Hierarchies 12 | 13 | Taking a structured JSON response and flattening it into one level 14 | 15 | - 2.2 Changing Hierarchies 16 | 17 | Taking a flat JSON response and decoding into a hierarchy of types in Swift 18 | 19 | - 3.1 Heterogeneous Arrays 20 | 21 | Decoding a feed of various subclasses of a common FeedItem base type. This is part 1 which solves the 22 | problem, but is somewhat complex. 23 | 24 | - 3.2 Heterogeneous Arrays 25 | 26 | Refactoring the previous example to create a reusable "class family" concept and an extension on 27 | KeyedDecodingContainer that makes decoding heterogeneous arrays much cleaner. 28 | 29 | - 4. Property Wrappers 30 | 31 | An example of using property wrappers to clean up some Codable issues that would otherwise require us 32 | to implement an entire Codable implementation just for one or two properties. 33 | 34 | Inspiration for this comes from [BetterCodable](https://github.com/marksands/BetterCodable) by Mark Sands. 35 | 36 | 37 | Presented to [iOS Conf SG](https://iosconf.sg) on January 21st, 2021. 38 | 39 | -------------------------------------------------------------------------------- /Advanced Codable.playground/Pages/2.1 Changing Hierarchies .xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let json = """ 4 | { 5 | "id": 123, 6 | "name": "Endeavor", 7 | "brewery": { 8 | "id": "sa001", 9 | "name": "Saint Arnold" 10 | } 11 | } 12 | """.data(using: .utf8)! 13 | 14 | struct Beer: Decodable { 15 | let id: Int 16 | let name: String 17 | let breweryId: String 18 | let breweryName: String 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case id 22 | case name 23 | case brewery 24 | } 25 | 26 | enum BreweryCodingKeys: String, CodingKey { 27 | case id 28 | case name 29 | } 30 | 31 | init(from decoder: Decoder) throws { 32 | let container = try decoder.container(keyedBy: CodingKeys.self) 33 | id = try container.decode(Int.self, forKey: .id) 34 | name = try container.decode(String.self, forKey: .name) 35 | 36 | let breweryContainer = try container.nestedContainer(keyedBy: BreweryCodingKeys.self, forKey: .brewery) 37 | breweryId = try breweryContainer.decode(String.self, forKey: .id) 38 | breweryName = try breweryContainer.decode(String.self, forKey: .name) 39 | } 40 | } 41 | 42 | let decoder = JSONDecoder() 43 | let beer = try! decoder.decode(Beer.self, from: json) 44 | 45 | dump(beer) 46 | -------------------------------------------------------------------------------- /Advanced Codable.playground/Pages/4. Property Wrappers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol DateValueCodableStrategy { 4 | associatedtype RawValue: Codable 5 | static func decode(_ value: RawValue) throws -> Date 6 | static func encode(_ date: Date) -> RawValue 7 | } 8 | 9 | struct ISO8601Strategy: DateValueCodableStrategy { 10 | typealias RawValue = String 11 | 12 | static func decode(_ value: String) throws -> Date { 13 | guard let date = ISO8601DateFormatter().date(from: value) else { 14 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid date format: \(value)")) 15 | } 16 | return date 17 | } 18 | 19 | static func encode(_ date: Date) -> String { 20 | ISO8601DateFormatter().string(from: date) 21 | } 22 | } 23 | 24 | struct YearMonthDayStrategy: DateValueCodableStrategy { 25 | typealias RawValue = String 26 | 27 | private static let dateFormatter: DateFormatter = { 28 | let dateFormatter = DateFormatter() 29 | dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 30 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 31 | dateFormatter.dateFormat = "y-MM-dd" 32 | return dateFormatter 33 | }() 34 | 35 | static func decode(_ value: String) throws -> Date { 36 | guard let date = dateFormatter.date(from: value) else { 37 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid date format: \(value)")) 38 | } 39 | return date 40 | } 41 | 42 | static func encode(_ date: Date) -> String { 43 | dateFormatter.string(from: date) 44 | } 45 | } 46 | 47 | @propertyWrapper struct DateValue: Codable { 48 | private let value: Formatter.RawValue 49 | var wrappedValue: Date 50 | 51 | init(wrappedValue: Date) { 52 | self.wrappedValue = wrappedValue 53 | self.value = Formatter.encode(wrappedValue) 54 | } 55 | 56 | init(from decoder: Decoder) throws { 57 | let container = try decoder.singleValueContainer() 58 | self.value = try container.decode(Formatter.RawValue.self) 59 | self.wrappedValue = try Formatter.decode(value) 60 | } 61 | } 62 | 63 | let json = """ 64 | { 65 | "name": "John", 66 | "dob": "1973-12-04", 67 | "joined_at": "2012-04-12T06:29:00Z" 68 | } 69 | """.data(using: .utf8)! 70 | 71 | struct User: Decodable { 72 | let name: String 73 | 74 | @DateValue 75 | var dob: Date 76 | 77 | @DateValue 78 | var joinedAt: Date 79 | } 80 | 81 | let decoder = JSONDecoder() 82 | decoder.keyDecodingStrategy = .convertFromSnakeCase 83 | 84 | let user = try decoder.decode(User.self, from: json) 85 | dump(user) 86 | -------------------------------------------------------------------------------- /Advanced Codable.playground/Pages/3.1 Heterogeneous Arrays.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let json = """ 4 | { 5 | "items": [ 6 | { 7 | "type": "text", 8 | "id": 55, 9 | "date": "2021-01-08T14:38:24Z", 10 | "text": "This is a text feed item" 11 | }, 12 | { 13 | "type": "image", 14 | "id": 56, 15 | "date": "2021-01-08T14:39:24Z", 16 | "image_url": "http://placekitten.com/200/300" 17 | } 18 | ] 19 | } 20 | """.data(using: .utf8)! 21 | 22 | class FeedItem: Decodable { 23 | let type: String 24 | let id: Int 25 | let date: Date 26 | } 27 | 28 | class TextFeedItem: FeedItem { 29 | let text: String 30 | 31 | enum CodingKeys: String, CodingKey { 32 | case text 33 | } 34 | 35 | required init(from decoder: Decoder) throws { 36 | let container = try decoder.container(keyedBy: CodingKeys.self) 37 | text = try container.decode(String.self, forKey: .text) 38 | try super.init(from: decoder) 39 | } 40 | } 41 | 42 | class ImageFeedItem: FeedItem { 43 | let imageUrl: URL 44 | 45 | enum CodingKeys: String, CodingKey { 46 | case imageUrl 47 | } 48 | 49 | required init(from decoder: Decoder) throws { 50 | let container = try decoder.container(keyedBy: CodingKeys.self) 51 | imageUrl = try container.decode(URL.self, forKey: .imageUrl) 52 | try super.init(from: decoder) 53 | } 54 | } 55 | 56 | struct AnyCodingKey: CodingKey { 57 | var stringValue: String 58 | init?(stringValue: String) { 59 | self.stringValue = stringValue 60 | } 61 | 62 | var intValue: Int? 63 | init?(intValue: Int) { 64 | stringValue = String(intValue) 65 | } 66 | } 67 | 68 | extension AnyCodingKey: ExpressibleByStringLiteral { 69 | init(stringLiteral value: StringLiteralType) { 70 | self.init(stringValue: value)! 71 | } 72 | } 73 | 74 | struct Feed: Decodable { 75 | let items: [FeedItem] 76 | 77 | enum CodingKeys: String, CodingKey { 78 | case items 79 | } 80 | 81 | init(from decoder: Decoder) throws { 82 | let container = try decoder.container(keyedBy: CodingKeys.self) 83 | var itemsContainer = try container.nestedUnkeyedContainer(forKey: .items) 84 | var itemsContainerCopy = itemsContainer 85 | 86 | var items: [FeedItem] = [] 87 | 88 | while !itemsContainer.isAtEnd { 89 | // peek at the type 90 | let typeContainer = try itemsContainer.nestedContainer(keyedBy: AnyCodingKey.self) 91 | let type = try typeContainer.decode(String.self, forKey: "type") 92 | switch type { 93 | case "text": 94 | let textFeedItem = try itemsContainerCopy.decode(TextFeedItem.self) 95 | items.append(textFeedItem) 96 | case "image": 97 | let imageFeedItem = try itemsContainerCopy.decode(ImageFeedItem.self) 98 | items.append(imageFeedItem) 99 | default: fatalError() 100 | } 101 | } 102 | 103 | self.items = items 104 | } 105 | } 106 | 107 | let decoder = JSONDecoder() 108 | decoder.keyDecodingStrategy = .convertFromSnakeCase 109 | decoder.dateDecodingStrategy = .iso8601 110 | 111 | let feed = try! decoder.decode(Feed.self, from: json) 112 | 113 | dump(feed) 114 | -------------------------------------------------------------------------------- /Advanced Codable.playground/Pages/3.2 Heterogeneous Arrays - Refactor.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let json = """ 4 | { 5 | "items": [ 6 | { 7 | "type": "text", 8 | "id": 55, 9 | "date": "2021-01-08T14:38:24Z", 10 | "text": "This is a text feed item" 11 | }, 12 | { 13 | "type": "image", 14 | "id": 56, 15 | "date": "2021-01-08T14:39:24Z", 16 | "image_url": "http://placekitten.com/200/300" 17 | } 18 | ] 19 | } 20 | """.data(using: .utf8)! 21 | 22 | protocol DecodableClassFamily: Decodable { 23 | associatedtype BaseType: Decodable 24 | static var discriminator: AnyCodingKey { get } 25 | func getType() -> BaseType.Type 26 | } 27 | 28 | enum FeedItemClassFamily: String, DecodableClassFamily { 29 | case text 30 | case image 31 | 32 | typealias BaseType = FeedItem 33 | static var discriminator: AnyCodingKey = "type" 34 | 35 | func getType() -> FeedItem.Type { 36 | switch self { 37 | case .text: return TextFeedItem.self 38 | case .image: return ImageFeedItem.self 39 | } 40 | } 41 | } 42 | 43 | extension KeyedDecodingContainer { 44 | func decodeHeterogeneousArray(family: Family.Type, forKey key: K) throws -> [Family.BaseType] { 45 | var itemsContainer = try nestedUnkeyedContainer(forKey: key) 46 | var itemsContainerCopy = itemsContainer 47 | 48 | var items: [Family.BaseType] = [] 49 | 50 | while !itemsContainer.isAtEnd { 51 | // peek at the type 52 | let typeContainer = try itemsContainer.nestedContainer(keyedBy: AnyCodingKey.self) 53 | let family = try typeContainer.decode(Family.self, forKey: Family.discriminator) 54 | let type = family.getType() 55 | let item = try itemsContainerCopy.decode(type) 56 | items.append(item) 57 | } 58 | 59 | return items 60 | } 61 | } 62 | 63 | class FeedItem: Decodable { 64 | let type: String 65 | let id: Int 66 | let date: Date 67 | } 68 | 69 | class TextFeedItem: FeedItem { 70 | let text: String 71 | 72 | enum CodingKeys: String, CodingKey { 73 | case text 74 | } 75 | 76 | required init(from decoder: Decoder) throws { 77 | let container = try decoder.container(keyedBy: CodingKeys.self) 78 | text = try container.decode(String.self, forKey: .text) 79 | try super.init(from: decoder) 80 | } 81 | } 82 | 83 | class ImageFeedItem: FeedItem { 84 | let imageUrl: URL 85 | 86 | enum CodingKeys: String, CodingKey { 87 | case imageUrl 88 | } 89 | 90 | required init(from decoder: Decoder) throws { 91 | let container = try decoder.container(keyedBy: CodingKeys.self) 92 | imageUrl = try container.decode(URL.self, forKey: .imageUrl) 93 | try super.init(from: decoder) 94 | } 95 | } 96 | 97 | struct AnyCodingKey: CodingKey { 98 | var stringValue: String 99 | init?(stringValue: String) { 100 | self.stringValue = stringValue 101 | } 102 | 103 | var intValue: Int? 104 | init?(intValue: Int) { 105 | stringValue = String(intValue) 106 | } 107 | } 108 | 109 | extension AnyCodingKey: ExpressibleByStringLiteral { 110 | init(stringLiteral value: StringLiteralType) { 111 | self.init(stringValue: value)! 112 | } 113 | } 114 | 115 | struct Feed: Decodable { 116 | let items: [FeedItem] 117 | 118 | enum CodingKeys: String, CodingKey { 119 | case items 120 | } 121 | 122 | init(from decoder: Decoder) throws { 123 | let container = try decoder.container(keyedBy: CodingKeys.self) 124 | items = try container.decodeHeterogeneousArray(family: FeedItemClassFamily.self, forKey: .items) 125 | } 126 | } 127 | 128 | let decoder = JSONDecoder() 129 | decoder.keyDecodingStrategy = .convertFromSnakeCase 130 | decoder.dateDecodingStrategy = .iso8601 131 | 132 | let feed = try! decoder.decode(Feed.self, from: json) 133 | 134 | dump(feed) 135 | --------------------------------------------------------------------------------