Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/marksands/BetterCodable
Better Codable through Property Wrappers
https://github.com/marksands/BetterCodable
codable property-wrappers swift-package-manager swift5-1
Last synced: 6 days ago
JSON representation
Better Codable through Property Wrappers
- Host: GitHub
- URL: https://github.com/marksands/BetterCodable
- Owner: marksands
- License: mit
- Created: 2019-10-19T03:12:48.000Z (about 5 years ago)
- Default Branch: master
- Last Pushed: 2023-11-23T14:20:03.000Z (12 months ago)
- Last Synced: 2024-10-29T17:56:08.041Z (10 days ago)
- Topics: codable, property-wrappers, swift-package-manager, swift5-1
- Language: Swift
- Homepage: http://marksands.github.io/2019/10/21/better-codable-through-property-wrappers.html
- Size: 57.6 KB
- Stars: 1,737
- Watchers: 18
- Forks: 80
- Open Issues: 12
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- fucking-awesome-swift - BetterCodable - 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. (Misc / Vim)
- awesome-swift - BetterCodable - 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. (Misc / Vim)
README
# Better Codable through Property Wrappers
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.
## @LossyArray
`@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.
### Usage
Easily filter nulls from primitive containers
```Swift
struct Response: Codable {
@LossyArray var values: [Int]
}let json = #"{ "values": [1, 2, null, 4, 5, null] }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)print(result) // [1, 2, 4, 5]
```Or silently exclude failable entities
```Swift
struct Failable: Codable {
let value: String
}struct Response: Codable {
@LossyArray var values: [Failable]
}let json = #"{ "values": [{"value": 4}, {"value": "fish"}] }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)print(result) // [Failable(value: "fish")]
```## @LossyDictionary
`@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.
### Usage
Easily filter nulls from primitive containers
```Swift
struct Response: Codable {
@LossyDictionary var values: [String: String]
}let json = #"{ "values": {"a": "A", "b": "B", "c": null } }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)print(result) // ["a": "A", "b": "B"]
```Or silently exclude failable entities
```Swift
struct Failable: Codable {
let value: String
}struct Response: Codable {
@LossyDictionary var values: [String: Failable]
}let json = #"{ "values": {"a": {"value": "A"}, "b": {"value": 2}} }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)print(result) // ["a": "A"]
```## @DefaultCodable
`@DefaultCodable` provides a generic property wrapper that allows for default values using a custom `DefaultCodableStrategy`. This allows one to implement their own default behavior for missing data and get the property wrapper behavior for free. Below are a few common default strategies, but they also serve as a template to implement a custom property wrapper to suit your specific use case.
While not provided in the source code, it's a sinch to create your own default strategy for your custom data flow.
```Swift
struct RefreshDaily: DefaultCodableStrategy {
static var defaultValue: CacheInterval { return CacheInterval.daily }
}struct Cache: Codable {
@DefaultCodable var refreshInterval: CacheInterval
}let json = #"{ "refreshInterval": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Cache.self, from: json)print(result) // Cache(refreshInterval: .daily)
```## @DefaultFalse
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.
`@DefaultFalse` mitigates the confusion by defaulting decoded Bools to false if the Decoder is unable to decode the value, either when null is encountered or some unexpected type.
### Usage
```Swift
struct UserPrivilege: Codable {
@DefaultFalse var isAdmin: Bool
}let json = #"{ "isAdmin": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)print(result) // UserPrivilege(isAdmin: false)
```## @DefaultEmptyArray
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.
`@DefaultEmptyArray` decodes Arrays and returns an empty array instead of nil if the Decoder is unable to decode the container.
### Usage
```Swift
struct Response: Codable {
@DefaultEmptyArray var favorites: [Favorite]
}let json = #"{ "favorites": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)print(result) // Response(favorites: [])
```## @DefaultEmptyDictionary
As mentioned previously, Optional Dictionaries are yet another container where nil and emptiness collide.
`@DefaultEmptyDictionary` decodes Dictionaries and returns an empty dictionary instead of nil if the Decoder is unable to decode the container.
### Usage
```Swift
struct Response: Codable {
@DefaultEmptyDictionary var scores: [String: Int]
}let json = #"{ "scores": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)print(result) // Response(values: [:])
```## @LosslessValue
All credit for this goes to [Ian Keen](https://twitter.com/iankay).
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.
`@LosslessValue` 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.
### Usage
```Swift
struct Response: Codable {
@LosslessValue var sku: String
@LosslessValue var isAvailable: Bool
}let json = #"{ "sku": 12345, "isAvailable": "true" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)print(result) // Response(sku: "12355", isAvailable: true)
```## Date Wrappers
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.
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 entities it decodes. The `@DateValue` wrapper is generic across a custom `DateValueCodableStrategy`. This allows anyone to implement their own date decoding strategy and get the property wrapper behavior for free. Below are a few common Date strategies, but they also serve as a template to implement a custom property wrapper to suit your specific date format needs.
The following property wrappers are heavily inspired by [Ian Keen](https://twitter.com/iankay).
## ISO8601Strategy
`ISO8601Strategy` 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.
### Usage
```Swift
struct Response: Codable {
@DateValue var date: Date
}let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)// 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).
```## RFC3339Strategy
`RFC3339Strategy` decodes RFC 3339 date strings into `Date`s. Encoding the date will encode the value back into the original string value.
### Usage
```Swift
struct Response: Codable {
@DateValue var date: Date
}let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)// 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).
```## TimestampStrategy
`TimestampStrategy` decodes `Double`s of a unix epoch into `Date`s. Encoding the date will encode the value into the original `TimeInterval` value.
### Usage
```Swift
struct Response: Codable {
@DateValue var date: Date
}let json = #"{ "date": 978307200.0 }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)// This produces a valid `Date` representing January 1st, 2001.
```## YearMonthDayStrategy
`@DateValue` 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.
### Usage
```Swift
struct Response: Codable {
@DateValue var date: Date
}let json = #"{ "date": "2001-01-01" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)// This produces a valid `Date` representing January 1st, 2001.
```Or lastly, you can mix and match date wrappers as needed where the benefits truly shine
```Swift
struct Response: Codable {
@DateValue var updatedAt: Date
@DateValue var birthday: Date
}let json = #"{ "updatedAt": "2019-10-19T16:14:32-05:00", "birthday": "1984-01-22" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)// This produces two valid `Date` values, `updatedAt` representing October 19, 2019 and `birthday` January 22nd, 1984.
```## Installation
### CocoaPods
```ruby
pod 'BetterCodable', '~> 0.1.0'
```### Swift Package Manager
## Attribution
This project is licensed under MIT. If you find these useful, please tell your boss where you found them.