├── 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 |
--------------------------------------------------------------------------------