'How to do custom transformations using Codable?

Let's say I need to transform a date string I received from a web service to a Date object.

Using ObjectMapper, that was easy:

class Example: Mappable {
    var date: Date?

    required init?(map: Map) { }

    func mapping(map: Map) {  
        date <- (map["date_of_interest"], GenericTransform().dateTransform)
    }
}

I just had to implement a tranformer ("GenericTransform" in this case) for date, and pass it as an argument along with the key name to decode.

Now, using Codable:

class Example2: Codable {
    var name: String
    var age: Int
    var date: Date?

    enum CodingKeys: String, CodingKey {
        case name, age
        case date = "date_of_interest"
    }
}

To transform a date, in my understanding, I'd have to either:

1) Pass a dateDecodingStrategy to my JSONDecoder, which I don't want to, because I'm trying to keep that part of the code as a generic function.

or

2) Implement an init(from decoder: Decoder) inside Example2, which I also don't want to, because of the boilerplate code I'd have to write to decode all the other properties (which would be automatically generated otherwise):

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    age = try container.decode(Int.self, forKey: .age)
    let dateString = try container.decode(String.self, forKey: .date)
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    if let date = formatter.date(from: dateString) {
        self.date = date
    } else {
        //throw error
    }
}

My question is: is there an easier way to do it than options 1 and 2 above? Maybe tweaking the CodingKeys enum somehow?

EDIT:

The problem is not only about dates, actually. In this project that I'm working on, there are many custom transformations being done using TransformOf<ObjectType, JSONType> from ObjectMapper.

For example, a color transformation of a hex code received from a web service into a UIColor is done using this bit of code:

let colorTransform = TransformOf<UIColor, String>(fromJSON: { (value) -> UIColor? in
    if let value = value {
        return UIColor().hexStringToUIColor(hex: value)
    }
    return nil
}, toJSON: { _ in
    return nil
})

I'm trying to remove ObjectMapper from the project, making these same transformations using Codable, so only using a custom dateDecodingStrategy will not suffice.

How would you guys do it? Implement a custom init(from decoder: Decoder) for every class that has to decode, for example, a color hex code?



Solution 1:[1]

Using dateDecodingStrategy in your case (as you only reference a single date format) is very simple…

let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)

Solution 2:[2]

We can use custom method for example like decodeAll here. Try in playground.

struct Model: Codable {
    var age: Int?
    var name: String?

    enum CodingKeys: String, CodingKey {
        case name
        case age
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decodeAll(String.self, forKey: .name)
        age = try container.decodeAll(Int.self, forKey: .age)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try? container.encode(name, forKey: .name)
        try? container.encode(age, forKey: .age)
    }
}

extension KeyedDecodingContainer where K: CodingKey, K: CustomDebugStringConvertible {
    func decodeAll<T: Decodable>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T {
        if let obj = try? decode(T.self, forKey: key) {
            return obj
        } else {
            if type == String.self {
                if let obj = try? decode(Int.self, forKey: key), let val = String(obj) as? T {
                    return val
                } else if let obj = try? decode(Double.self, forKey: key), let val = String(obj) as? T {
                    return val
                }
            } else if type == Int.self {
                if let obj = try? decode(String.self, forKey: key), let val = Int(obj) as? T {
                    return val
                } else if let obj = try? decode(Double.self, forKey: key), let val = Int(obj) as? T {
                    return val
                }
            } else if type == Double.self {
                if let obj = try? decode(String.self, forKey: key), let val = Double(obj) as? T {
                    return val
                } else if let obj = try? decode(Int.self, forKey: key), let val = Double(obj) as? T {
                    return val
                }
            }
        }
        throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Wrong type for: \(key.stringValue)"))
    }
}

let json = ##"{ "age": "5", "name": 98 }"##
do {
    let obj = try JSONDecoder().decode(Model.self, from: json.data(using: .utf8)!)
    print(obj)
} catch {
    print(error)
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ashley Mills
Solution 2