Decoding JSON having multiple Date formats using @propertyWrapper transformers

dgrzeszczak

dgrzeszczak

Posted on October 14, 2019

Decoding JSON having multiple Date formats using @propertyWrapper transformers

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:

{
    "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:

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:

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

But you probably wonder how Transformers look like ?

Transformer

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

protocol DecodableTransformer {
    associatedtype Source: Decodable
    associatedtype Object

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

and finally we can look on our @propertyWrapper implementation

@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

Or you can just look at 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
💖 💪 🙅 🚩
dgrzeszczak
dgrzeszczak

Posted on October 14, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related