Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

Awesome Lists containing this project

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.