Decoding JSON having multiple Date formats using @propertyWrapper transformers
dgrzeszczak
Posted on October 14, 2019
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 toDate
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 ?
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.
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
Posted on October 14, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.