|
| 1 | +# Better Codable through Property Wrappers |
| 2 | + |
| 3 | +Level up your `Codable` structs through property wrappers. The goal of these property wrappers is to avoid implementing a custom `init(from decoder: Decoder) throws` and suffer through boilerplate. |
| 4 | + |
| 5 | +## @LossyArray |
| 6 | + |
| 7 | +`@LossyArray` decodes Arrays and filters invalid values if the Decoder is unable to decode the value. This is useful when the Array contains non-optional types and your API serves elements that are either null or fail to decode within the container. |
| 8 | + |
| 9 | +### Usage |
| 10 | + |
| 11 | +Easily filter nulls from primitive containers |
| 12 | + |
| 13 | +```Swift |
| 14 | +struct Response: Codable { |
| 15 | + @LossyArray var values: [Int] |
| 16 | +} |
| 17 | + |
| 18 | +let json = #"{ "values": [1, 2, null, 4, 5, null] }"#.data(using: .utf8)! |
| 19 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 20 | + |
| 21 | +print(result) // [1, 2, 4, 5] |
| 22 | +``` |
| 23 | + |
| 24 | +Or silently exclude failable entities |
| 25 | + |
| 26 | +```Swift |
| 27 | +struct Failable: Codable { |
| 28 | + let value: String |
| 29 | +} |
| 30 | + |
| 31 | +struct Response: Codable { |
| 32 | + @LossyArray var values: [Failable] |
| 33 | +} |
| 34 | + |
| 35 | +let json = #"{ "values": [{"value": 4}, {"value": "fish"}] }"#.data(using: .utf8)! |
| 36 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 37 | + |
| 38 | +print(result) // [Failable(value: "fish")] |
| 39 | +``` |
| 40 | + |
| 41 | +## @LossyDictionary |
| 42 | + |
| 43 | +`@LossyDictionary` decodes Dictionaries and filters invalid key-value pairs if the Decoder is unable to decode the value. This is useful if the Dictionary is intended to contain non-optional values and your API serves values that are either null or fail to decode within the container. |
| 44 | + |
| 45 | +### Usage |
| 46 | + |
| 47 | +Easily filter nulls from primitive containers |
| 48 | + |
| 49 | +```Swift |
| 50 | +struct Response: Codable { |
| 51 | + @LossyDictionary var values: [String: String] |
| 52 | +} |
| 53 | + |
| 54 | +let json = #"{ "values": {"a": "A", "b": "B", "c": null } }"#.data(using: .utf8)! |
| 55 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 56 | + |
| 57 | +print(result) // ["a": "A", "b": "B"] |
| 58 | +``` |
| 59 | + |
| 60 | +Or silently exclude failable entities |
| 61 | + |
| 62 | +```Swift |
| 63 | +struct Failable: Codable { |
| 64 | + let value: String |
| 65 | +} |
| 66 | + |
| 67 | +struct Response: Codable { |
| 68 | + @LossyDictionary var values: [String: Failable] |
| 69 | +} |
| 70 | + |
| 71 | +let json = #"{ "values": {"a": {"value": "A"}, "b": {"value": 2}} }"#.data(using: .utf8)! |
| 72 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 73 | + |
| 74 | +print(result) // ["a": "A"] |
| 75 | +``` |
| 76 | + |
| 77 | +## @DefaultFalse |
| 78 | + |
| 79 | +Optional Bools are weird. A type that once meant true or false, now has three possible states: `.some(true)`, `.some(false)`, or `.none`. And the `.none` condition _could_ indicate truthiness if BadDecisions™ were made. |
| 80 | + |
| 81 | +`@DefaultFalse` mitigates the confusion by defaulting decoded Bools to false if the Decoder is unable to decode the value, eitehr when null is encountered or some unexpected type. |
| 82 | + |
| 83 | +### Usage |
| 84 | + |
| 85 | +```Swift |
| 86 | +struct UserPrivilege: Codable { |
| 87 | + @DefaultFalse var isAdmin: Bool |
| 88 | +} |
| 89 | + |
| 90 | +let json = #"{ "isAdmin": null }"#.data(using: .utf8)! |
| 91 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 92 | + |
| 93 | +print(result) // UserPrivilege(isAdmin: false) |
| 94 | +``` |
| 95 | + |
| 96 | +## @DefaultEmptyArray |
| 97 | + |
| 98 | +The weirdness of Optional Booleans extends to other types, such as Arrays. Soroush has a [great blog post](http://khanlou.com/2016/10/emptiness/) explaining why you may want to avoid Optional Arrays. Unfortunately, this idea doesn't come for free in Swift out of the box. Being forced to implement a custom initializer in order to nil coalesce nil arrays to empty arrays is no fun. |
| 99 | + |
| 100 | +`@DefaultEmptyArray` decodes Arrays and returns an empty array instead of nil if the Decoder is unable to decode the container. |
| 101 | + |
| 102 | +### Usage |
| 103 | + |
| 104 | +```Swift |
| 105 | +struct Response: Codable { |
| 106 | + @DefaultEmptyArray var favorites: [Favorite] |
| 107 | +} |
| 108 | + |
| 109 | +let json = #"{ "favorites": null }"#.data(using: .utf8)! |
| 110 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 111 | + |
| 112 | +print(result) // Response(favorites: []) |
| 113 | +``` |
| 114 | + |
| 115 | +## @DefaultEmptyDictionary |
| 116 | + |
| 117 | +As mentioned previously, Optional Dictionaries are yet another container where nil and emptiness collide. |
| 118 | + |
| 119 | + `@DefaultEmptyDictionary` decodes Dictionaries and returns an empty dictionary instead of nil if the Decoder is unable to decode the container. |
| 120 | + |
| 121 | +### Usage |
| 122 | + |
| 123 | +```Swift |
| 124 | +struct Response: Codable { |
| 125 | + @DefaultEmptyDictionary var scores: [String: Int] |
| 126 | +} |
| 127 | + |
| 128 | +let json = #"{ "scores": null }"#.data(using: .utf8)! |
| 129 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 130 | + |
| 131 | +print(result) // Response(values: [:]) |
| 132 | +``` |
| 133 | + |
| 134 | +## @LosslessValue |
| 135 | + |
| 136 | +All credit for this goes to [Ian Keen](https://twitter.com/iankay). |
| 137 | + |
| 138 | +Somtimes APIs can be unpredictable. They may treat some form of Identifiers or SKUs as `Int`s for one response and `String`s for another. Or you might find yourself encountering `"true"` when you expect a boolean. This is where `@LosslessValue` comes into play. |
| 139 | + |
| 140 | +`@LesslessValue` will attempt to decode a value into the type that you expect, preserving the data that would otherwise throw an exception or be lost altogether. |
| 141 | + |
| 142 | +### Usage |
| 143 | + |
| 144 | +```Swift |
| 145 | +struct Response: Codable { |
| 146 | + @LosslessValue var sku: String |
| 147 | + @LosslessValue var isAvailable: Bool |
| 148 | +} |
| 149 | + |
| 150 | +let json = #"{ "sku": 12345, "isAvailable": "true" }"#.data(using: .utf8)! |
| 151 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 152 | + |
| 153 | +print(result) // Response(sku: "12355", isAvailable: true) |
| 154 | +``` |
| 155 | + |
| 156 | +## Date Wrappers |
| 157 | + |
| 158 | +One common frustration with `Codable` is decoding entities that have mixed date formats. `JSONDecoder` comes built in with a handy `dateDecodingStrategy` property, but that uses the same date format for all dates that it will decode. And often, `JSONDecoder` lives elsewhere from the entity forcing tight coupling with the entities if you choose to use its date decoding strategy. |
| 159 | + |
| 160 | +Property wrappers are a nice solution to the aforementioned issues. It allows tight binding of the date formatting strategy directly with the property of the entity, and allows the `JSONDecoder` to remain decoupled from the enties it decodes. Below are a few common Date strategies, but they also serve as a template to implement a custom property wrapper to suit your spcific date format needs. |
| 161 | + |
| 162 | +The following property wrappers are heavily inspired by [Ian Keen](https://twitter.com/iankay). |
| 163 | + |
| 164 | +## @ISO8601Date |
| 165 | + |
| 166 | +`@ISO8601Date` relies on an `ISO8601DateFormatter` in order to decode `String` values into `Date`s. Encoding the date will encode the value into the original string value. |
| 167 | + |
| 168 | +### Usage |
| 169 | + |
| 170 | +```Swift |
| 171 | +struct Response: Codable { |
| 172 | + @ISO8601Date var date: Date |
| 173 | +} |
| 174 | + |
| 175 | +let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)! |
| 176 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 177 | + |
| 178 | +// This produces a valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time). |
| 179 | +``` |
| 180 | + |
| 181 | +## @RFC3339Date |
| 182 | + |
| 183 | +`@RFC3339Date` decodes RFC 3339 date strings into `Date`s. Encoding the date will encode the value back into the original string value. |
| 184 | + |
| 185 | +### Usage |
| 186 | + |
| 187 | +```Swift |
| 188 | +struct Response: Codable { |
| 189 | + @RFC3339Date var date: Date |
| 190 | +} |
| 191 | + |
| 192 | +let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)! |
| 193 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 194 | + |
| 195 | +// This produces a valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time). |
| 196 | +``` |
| 197 | + |
| 198 | +## @TimestampDate |
| 199 | + |
| 200 | +`@TimestampDate` decodes `Double`s of a unix epoch into `Date`s. Encoding the date will encode the value into the original `TimeInterval` value. |
| 201 | + |
| 202 | +### Usage |
| 203 | + |
| 204 | +```Swift |
| 205 | +struct Response: Codable { |
| 206 | + @TimestampDate var date: Date |
| 207 | +} |
| 208 | + |
| 209 | +let json = #"{ "date": 978307200.0 }"#.data(using: .utf8)! |
| 210 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 211 | + |
| 212 | +// This produces a valid `Date` representing January 1st, 2001. |
| 213 | +``` |
| 214 | + |
| 215 | +## @YearMonthDayDate |
| 216 | + |
| 217 | + `@YearMonthDayDate` decodes string values into `Date`s using the date format `y-MM-dd`. Encoding the date will encode the value back into the original string format. |
| 218 | + |
| 219 | +### Usage |
| 220 | + |
| 221 | +```Swift |
| 222 | +struct Response: Codable { |
| 223 | + @YearMonthDayDate var date: Date |
| 224 | +} |
| 225 | + |
| 226 | +let json = #"{ "date": "2001-01-01" }"#.data(using: .utf8)! |
| 227 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 228 | + |
| 229 | +// This produces a valid `Date` representing January 1st, 2001. |
| 230 | +``` |
| 231 | + |
| 232 | +Or lastly, you can mix and match date wrappers as needed where the benefits truly shine |
| 233 | + |
| 234 | +```Swift |
| 235 | +struct Response: Codable { |
| 236 | + @ISO8601Date var updatedAt: Date |
| 237 | + @YearMonthDayDate var birthday: Date |
| 238 | +} |
| 239 | + |
| 240 | +let json = #"{ "updatedAt": "2019-10-19T16:14:32-05:00", "birthday": "1984-01-22" }"#.data(using: .utf8)! |
| 241 | +let result = try JSONDecoder().decode(Response.self, from: json) |
| 242 | + |
| 243 | +// This produces two valid `Date` values, `updatedAt` representing October 19, 2019 and `birthday` January 22nd, 1984. |
| 244 | +``` |
| 245 | + |
| 246 | +## Installation |
| 247 | + |
| 248 | +Swift Package Manager |
| 249 | + |
| 250 | +### Attribution |
| 251 | + |
| 252 | +This project is licensed under MIT. If you find these useful, please tell your boss where you found them. |
0 commit comments