1

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?

Rod
  • 424
  • 2
  • 7
  • 17
  • 4
    "1) Pass a dateDecodingStrategy to my JSONDecoder, which I don't want to" Why not? – Leo Dabus Aug 21 '18 at 14:42
  • If you need a flexible date decoding strategy this might be what you are looking for https://stackoverflow.com/a/46458771/2303865 – Leo Dabus Aug 21 '18 at 14:47
  • @LeoDabus I updated the question, please take a look :) – Rod Aug 21 '18 at 15:57
  • 1
    Forget about object mapping and focus your efforts in learning how to properly encode/decode your JSON each case individually. You should take some time and read Reflecting on Reflection https://developer.apple.com/swift/blog/?id=37 – Leo Dabus Aug 21 '18 at 16:00
  • @LeoDabus Thanks for answering, man. That's what I'm trying to do, but what is the right way to do it? Manually decode using `init(from decoder: Decoder)` when these custom transformations are needed? – Rod Aug 21 '18 at 16:03
  • I have posted apple developer blog link about working JSON. For a custom decoder we would need to know the actual purpose – Leo Dabus Aug 21 '18 at 16:05
  • 1
    This might help but again the question as it is It is too broad https://stackoverflow.com/questions/48493317/how-to-extend-float3-or-any-other-built-in-type-to-conform-to-the-codable-protoc/48494133#48494133 – Leo Dabus Aug 21 '18 at 16:08
  • This last link might be the way to do it. I also read the apple blog post, but I still need to get a better understanding of Reflection. Thanks for the help! – Rod Aug 21 '18 at 16:45
  • 1
    regarding making UIColor codable https://stackoverflow.com/questions/46522572/initializer-requirement-initjson-can-only-be-satisfied-by-a-required-init or https://stackoverflow.com/questions/48566443/implementing-codable-for-uicolor – Leo Dabus Aug 21 '18 at 16:48

2 Answers2

3

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)
Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
0

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)
}
ChanOnly123
  • 1,004
  • 10
  • 12