Decoding JSON having multiple Date formats using @propertyWrapper transformers
Tags: ios, swift, codable, propertywrapper
Published: 2019-10-14 23:10:41

Decoding JSON having multiple Date formats requires manual implementation of Decodable and of course that means a lot boilerplate code. Swift 5.1 give us new toy called @propertyWrapper. It is additional layer on our properties and we will use it to add transformation layer during JSON decoding process.

So let's assume we have some simple JSON like that:

```json
{
    "date": "2019-10-20",
    "dateTime": "2019-11-02T19:45:00"
}
```

As we can see we have two different date formats: simple date and date with time. Looking into JSONDecoder documentation we can see there is a possibility to set only one dateDecodingStrategy so how we can solve that problem ? So far we had two options:
- decode whole JSON manually
- decode dates to `String` and use some properties/functions to transform it to `Date` later. 

But as I mention from Swift 5.1 we have new toy so let's try to set date formatter per each property. Decodable model will look like that:

```swift
struct DatesExample: Decodable {
    @DecodedBy<DateTransformer> var date: Date
    @DecodedBy<DateTransformerWithTime> var dateTime: Date
}
```
what means `date` property will be decoded by `DateTransformer` and `dateTime` property will be decoded by `DateTransformerWithTime`. 

And of course we will use standard JSONDecoder to decode it:

```swift
try JSONDecoder().decode(DatesExample.self, from: data)
```

But you probably wonder how Transformers look like ?

![Transformer](https://media.giphy.com/media/6ahZ2FgE1cN8s/giphy.gif)


```swift
enum DateTransformer: DecodableTransformer {

    static func transform(from decodable: String) throws -> Date {

        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"

        guard let date = formatter.date(from: decodable) else {
            throw DecodingError.transformFailed
        }

        return date
    }
}

enum DateTransformerWithTime: DecodableTransformer {

    static func transform(from decodable: String) throws -> Date {

        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"

        guard let date = formatter.date(from: decodable) else {
            throw DecodingError.transformFailed
        }

        return date
    }
}
```

The implementation looks really easy. We have to implement `transform` method that takes String as a parameter, and return `Date` using proper `DateFormatter`. 

Ok, so we have all the code for decoding our JSON but how DecodedBy is implemented ? 

First we need to know what `DecodableTransformer` is

```swift
protocol DecodableTransformer {
    associatedtype Source: Decodable
    associatedtype Object

    static func transform(from decodable: Source) throws -> Object
}
```

and finally we can look on our @propertyWrapper implementation 

```swift
@propertyWrapper
struct DecodedBy<T: DecodableTransformer>: Decodable {
    var wrappedValue: T.Object

    init(from decoder: Decoder) throws {
        let source = try T.Source(from: decoder)
        wrappedValue = try T.transform(from: source)
    }
}
```

That's really it! That couple lines of code allow us to apply custom transformation during decoding for each property in the model separately. What's important transformed property don't even has to be Decodable but still the whole process will work properly! 

As an excercise you can write @EncodedBy equivalent for Encodable or @CodedBy for two way coding that is a little bit more tricky. 

![Surprise](https://thumbs.gfycat.com/DisastrousFocusedFlea-size_restricted.gif)

Or you can just look at [KeyedCodable](https://github.com/dgrzeszczak/KeyedCodable) library that have it already implemented with other useful stuff like:
- easy nested key mappings
- @Zero property wrapper that sets default zero value instead of nil value
- @Flat for safe array decoding - prevents decoding array fails if decoding single element fails
- @Flat for structuring big flat jsons into smaller models